fix: align group management APIs
This commit is contained in:
@@ -51,6 +51,17 @@ type groupFanoutEnvelope struct {
|
||||
Message json.RawMessage `json:"message"`
|
||||
}
|
||||
|
||||
type RealtimeFanoutEnvelope struct {
|
||||
OriginNodeID string `json:"origin_node_id"`
|
||||
TenantID types.TenantID `json:"tenant_id"`
|
||||
Kind string `json:"kind"`
|
||||
TargetType string `json:"target_type"`
|
||||
Target string `json:"target"`
|
||||
SkipSid string `json:"skip_sid,omitempty"`
|
||||
DedupKey string `json:"dedup_key,omitempty"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
}
|
||||
|
||||
type groupMessageRuntime interface {
|
||||
Close() error
|
||||
Claim(tenantID types.TenantID, topic string, from types.Uid, clientMsgID string) (*groupMessageUniqueRecord, bool, error)
|
||||
@@ -71,6 +82,8 @@ type groupMessageRuntime interface {
|
||||
GetTopicNodeOnlineMembers(tenantID types.TenantID, topic, nodeID string) ([]types.Uid, error)
|
||||
PublishGroupFanout(originNodeID string, message any) error
|
||||
StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error
|
||||
PublishRealtimeFanout(originNodeID string, tenantID types.TenantID, kind, targetType, target, skipSid, dedupKey string, message any) error
|
||||
StartRealtimeFanoutSubscriber(ctx context.Context, nodeID string, handler func(*RealtimeFanoutEnvelope)) error
|
||||
CacheObject(tenantID types.TenantID, namespace, id string, value any) error
|
||||
GetCachedObject(tenantID types.TenantID, namespace, id string, value any) (bool, error)
|
||||
DeleteCachedObject(tenantID types.TenantID, namespace, id string) error
|
||||
@@ -237,6 +250,20 @@ func StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func
|
||||
return groupMsgRuntime.StartGroupFanoutSubscriber(ctx, nodeID, handler)
|
||||
}
|
||||
|
||||
func PublishRealtimeFanout(originNodeID string, tenantID types.TenantID, kind, targetType, target, skipSid, dedupKey string, message any) error {
|
||||
if groupMsgRuntime == nil || originNodeID == "" || !tenantID.IsValid() || targetType == "" || target == "" || message == nil {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.PublishRealtimeFanout(originNodeID, tenantID, kind, targetType, target, skipSid, dedupKey, message)
|
||||
}
|
||||
|
||||
func StartRealtimeFanoutSubscriber(ctx context.Context, nodeID string, handler func(*RealtimeFanoutEnvelope)) error {
|
||||
if groupMsgRuntime == nil || nodeID == "" || handler == nil {
|
||||
return nil
|
||||
}
|
||||
return groupMsgRuntime.StartRealtimeFanoutSubscriber(ctx, nodeID, handler)
|
||||
}
|
||||
|
||||
func CacheObject(tenantID types.TenantID, namespace, id string, value any) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || namespace == "" || id == "" || value == nil {
|
||||
return nil
|
||||
@@ -761,28 +788,49 @@ func (r *redisGroupMessageRuntime) GetTopicNodeOnlineMembers(tenantID types.Tena
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) PublishGroupFanout(originNodeID string, message any) error {
|
||||
return r.PublishRealtimeFanout(originNodeID, types.ZeroTenantID, "message", "topic", "", "", "", message)
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) PublishRealtimeFanout(originNodeID string, tenantID types.TenantID, kind, targetType, target, skipSid, dedupKey string, message any) error {
|
||||
if !r.cfg.GroupFanout {
|
||||
return nil
|
||||
}
|
||||
if originNodeID == "" || message == nil {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envelope, err := json.Marshal(groupFanoutEnvelope{
|
||||
envelope, err := json.Marshal(RealtimeFanoutEnvelope{
|
||||
OriginNodeID: originNodeID,
|
||||
TenantID: tenantID,
|
||||
Kind: kind,
|
||||
TargetType: targetType,
|
||||
Target: target,
|
||||
SkipSid: skipSid,
|
||||
DedupKey: dedupKey,
|
||||
Message: payload,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.Publish(context.Background(), r.groupFanoutChannel(), envelope).Err()
|
||||
return r.client.Publish(context.Background(), r.realtimeFanoutChannel(tenantID), envelope).Err()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) StartGroupFanoutSubscriber(ctx context.Context, nodeID string, handler func([]byte)) error {
|
||||
return r.StartRealtimeFanoutSubscriber(ctx, nodeID, func(envelope *RealtimeFanoutEnvelope) {
|
||||
if envelope != nil {
|
||||
handler(envelope.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) StartRealtimeFanoutSubscriber(ctx context.Context, nodeID string, handler func(*RealtimeFanoutEnvelope)) error {
|
||||
if !r.cfg.GroupFanout {
|
||||
return nil
|
||||
}
|
||||
pubsub := r.client.Subscribe(ctx, r.groupFanoutChannel())
|
||||
pubsub := r.client.PSubscribe(ctx, r.realtimeFanoutPattern())
|
||||
if _, err := pubsub.Receive(ctx); err != nil {
|
||||
_ = pubsub.Close()
|
||||
return err
|
||||
@@ -798,11 +846,27 @@ func (r *redisGroupMessageRuntime) StartGroupFanoutSubscriber(ctx context.Contex
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var envelope groupFanoutEnvelope
|
||||
var envelope RealtimeFanoutEnvelope
|
||||
if err := json.Unmarshal([]byte(msg.Payload), &envelope); err != nil || envelope.OriginNodeID == nodeID || len(envelope.Message) == 0 {
|
||||
continue
|
||||
}
|
||||
handler(envelope.Message)
|
||||
if envelope.TenantID.IsValid() && envelope.DedupKey != "" {
|
||||
id := fmt.Sprintf("%d:%s", envelope.TenantID, envelope.DedupKey)
|
||||
claimed, err := r.ClaimShortState("rt_dedup", id, 5*time.Minute)
|
||||
if err != nil || !claimed {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if envelope.TargetType == "" {
|
||||
envelope.TargetType = "topic"
|
||||
}
|
||||
if envelope.Kind == "" {
|
||||
envelope.Kind = "message"
|
||||
}
|
||||
if !envelope.TenantID.IsValid() && envelope.Target == "" {
|
||||
continue
|
||||
}
|
||||
handler(&envelope)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -1035,6 +1099,17 @@ func (r *redisGroupMessageRuntime) groupFanoutChannel() string {
|
||||
return r.key(types.ZeroTenantID, "group_fanout")
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) realtimeFanoutChannel(tenantID types.TenantID) string {
|
||||
if tenantID.IsValid() {
|
||||
return r.key(tenantID, "rt", "fanout")
|
||||
}
|
||||
return r.groupFanoutChannel()
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) realtimeFanoutPattern() string {
|
||||
return strings.TrimSuffix(r.prefix, ":") + ":*:rt:fanout"
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) objectCacheKey(tenantID types.TenantID, namespace, id string) string {
|
||||
return r.key(tenantID, "cache", namespace, id)
|
||||
}
|
||||
|
||||
@@ -113,6 +113,14 @@ func (r *testGroupMessageRuntime) StartGroupFanoutSubscriber(context.Context, st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *testGroupMessageRuntime) PublishRealtimeFanout(string, types.TenantID, string, string, string, string, string, any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *testGroupMessageRuntime) StartRealtimeFanoutSubscriber(context.Context, string, func(*RealtimeFanoutEnvelope)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *testGroupMessageRuntime) CacheObject(types.TenantID, string, string, any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,9 +11,6 @@ type HTTPInfrastructurePersistenceInterface interface {
|
||||
BeginIdempotency(types.TenantID, *types.HTTPIdempotencyRecord) (*types.HTTPIdempotencyRecord, bool, error)
|
||||
CompleteIdempotency(types.TenantID, *types.HTTPIdempotencyRecord) error
|
||||
AbortIdempotency(types.TenantID, *types.HTTPIdempotencyRecord) error
|
||||
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)
|
||||
@@ -64,30 +61,6 @@ func (httpInfrastructureMapper) AbortIdempotency(tenantID types.TenantID, record
|
||||
return infrastructure.TenantHTTPIdempotencyAbort(tenantID, record)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) CreateEvent(tenantID types.TenantID, event *types.BusinessEvent) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return infrastructure.TenantBusinessEventCreate(tenantID, event)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) ListPendingEvents(tenantID types.TenantID, limit int, now time.Time) ([]types.BusinessEvent, error) {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infrastructure.TenantBusinessEventListPending(tenantID, limit, now)
|
||||
}
|
||||
|
||||
func (httpInfrastructureMapper) MarkEventDelivered(tenantID types.TenantID, eventID string, now time.Time) error {
|
||||
infrastructure, err := tenantHTTPInfrastructureAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
|
||||
+20
-24
@@ -1620,6 +1620,7 @@ type GroupBusinessPersistenceInterface interface {
|
||||
CreateSettings(types.TenantID, *types.GroupSettings) (*types.GroupSettings, error)
|
||||
GetSettings(types.TenantID, string) (*types.GroupSettings, error)
|
||||
UpdateSettings(types.TenantID, string, types.GroupSettingsPatch) (*types.GroupSettings, error)
|
||||
Dissolve(types.TenantID, string, types.Uid, string, time.Time) (*types.GroupSettings, error)
|
||||
UpsertMember(types.TenantID, *types.GroupMember) (*types.GroupMember, error)
|
||||
GetMember(types.TenantID, string, types.Uid) (*types.GroupMember, error)
|
||||
ListMembers(types.TenantID, string, types.GroupMemberListOptions) ([]types.GroupMember, string, error)
|
||||
@@ -1644,8 +1645,6 @@ type GroupBusinessPersistenceInterface interface {
|
||||
UpdateRole(types.TenantID, string, types.Uid, types.Uid, string, types.AccessMode, time.Time) error
|
||||
TransferOwner(types.TenantID, string, types.Uid, types.Uid, int, time.Time) error
|
||||
CanPost(types.TenantID, string, types.Uid, bool, time.Time) (bool, error)
|
||||
ListPendingEvents(types.TenantID, int, time.Time) ([]types.GroupEvent, error)
|
||||
MarkEventDelivered(types.TenantID, string, time.Time) error
|
||||
}
|
||||
|
||||
type groupBusinessMapper struct{}
|
||||
@@ -1721,6 +1720,25 @@ func (groupBusinessMapper) UpdateSettings(tenantID types.TenantID, topic string,
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (groupBusinessMapper) Dissolve(tenantID types.TenantID, topic string, actor types.Uid, reason string, now time.Time) (*types.GroupSettings, error) {
|
||||
if strings.TrimSpace(topic) == "" || actor.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings, err := tenantAdp.TenantGroupDissolve(tenantID, strings.TrimSpace(topic), actor, reason, now)
|
||||
if err == nil {
|
||||
if settings != nil {
|
||||
_ = CacheObject(tenantID, cacheNamespaceGroupSettings, strings.TrimSpace(topic), settings)
|
||||
} else {
|
||||
_ = DeleteCachedObject(tenantID, cacheNamespaceGroupSettings, strings.TrimSpace(topic))
|
||||
}
|
||||
}
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (groupBusinessMapper) UpsertMember(tenantID types.TenantID, member *types.GroupMember) (*types.GroupMember, error) {
|
||||
if member == nil || member.Topic == "" || member.UserID.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
@@ -2096,28 +2114,6 @@ func (groupBusinessMapper) CanPost(tenantID types.TenantID, topic string, uid ty
|
||||
return tenantAdp.TenantGroupCanPost(tenantID, strings.TrimSpace(topic), uid, admin, now)
|
||||
}
|
||||
|
||||
func (groupBusinessMapper) ListPendingEvents(tenantID types.TenantID, limit int, now time.Time) ([]types.GroupEvent, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantGroupEventListPending(tenantID, limit, now)
|
||||
}
|
||||
|
||||
func (groupBusinessMapper) MarkEventDelivered(tenantID types.TenantID, eventID string, now time.Time) error {
|
||||
if strings.TrimSpace(eventID) == "" || now.IsZero() {
|
||||
return types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tenantAdp.TenantGroupEventMarkDelivered(tenantID, strings.TrimSpace(eventID), now)
|
||||
}
|
||||
|
||||
func groupMemberCacheID(topic string, uid types.Uid) string {
|
||||
return strings.TrimSpace(topic) + ":" + uid.String()
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ type GroupSettingsPatch struct {
|
||||
MemberUploadFiles *bool
|
||||
MemberPrivateChat *bool
|
||||
MemberInvite *bool
|
||||
MaxMemberCount *int
|
||||
MaxAdminCount *int
|
||||
MemberCountDelta int
|
||||
Version int
|
||||
}
|
||||
@@ -209,19 +211,3 @@ type GroupFile struct {
|
||||
UploaderRaw int64 `db:"uploader_id"`
|
||||
DeletedByRaw *int64 `db:"deleted_by"`
|
||||
}
|
||||
|
||||
type GroupEvent struct {
|
||||
ID int64 `db:"id"`
|
||||
EventID string `db:"event_id"`
|
||||
Topic string `db:"topic"`
|
||||
ActorUserID Uid `db:"-"`
|
||||
EventType string `db:"event_type"`
|
||||
Payload []byte `db:"payload"`
|
||||
Status string `db:"status"`
|
||||
Attempts int `db:"attempts"`
|
||||
NextRetryAt *time.Time `db:"next_retry_at"`
|
||||
DeliveredAt *time.Time `db:"delivered_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
ActorRaw int64 `db:"actor_user_id"`
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ const (
|
||||
HTTPIdempotencyProcessing = "processing"
|
||||
HTTPIdempotencyCompleted = "completed"
|
||||
|
||||
BusinessEventPending = "pending"
|
||||
BusinessEventDelivered = "delivered"
|
||||
|
||||
KafkaConsumeProcessing = "processing"
|
||||
KafkaConsumeSucceeded = "succeeded"
|
||||
KafkaConsumeFailed = "failed"
|
||||
@@ -40,23 +37,6 @@ type HTTPIdempotencyRecord struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// BusinessEvent is a durable event destined for an online user or topic.
|
||||
type BusinessEvent struct {
|
||||
ID int64
|
||||
EventID string
|
||||
TargetType string
|
||||
Target string
|
||||
ActorUserID Uid
|
||||
EventType string
|
||||
Payload []byte
|
||||
Status string
|
||||
Attempts int
|
||||
NextRetryAt *time.Time
|
||||
DeliveredAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// KafkaConsumeRecord stores idempotent consumer state for one event and group.
|
||||
type KafkaConsumeRecord struct {
|
||||
EventID string
|
||||
|
||||
Reference in New Issue
Block a user