MM-52600: [Shared Channels] Shared channels do not sync channel membership (#30976)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0082e3e94d
Коммит
fa1c77d9b0
@@ -89,6 +89,7 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc
|
||||
RemoteId: rc.RemoteId,
|
||||
IsInviteAccepted: true,
|
||||
IsInviteConfirmed: false,
|
||||
LastMembersSyncAt: 0,
|
||||
}
|
||||
if _, err = scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil {
|
||||
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error saving channel invite for %s: %v", rc.DisplayName, err))
|
||||
@@ -134,6 +135,7 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc
|
||||
}
|
||||
|
||||
curTime := model.GetMillis()
|
||||
var sharedChannelRemote *model.SharedChannelRemote
|
||||
if existingScr != nil {
|
||||
if existingScr.DeleteAt == 0 && existingScr.IsInviteConfirmed {
|
||||
// the shared channel remote exists and is not
|
||||
@@ -153,6 +155,7 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc
|
||||
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error confirming channel invite for %s: %v", rc.DisplayName, sErr))
|
||||
return
|
||||
}
|
||||
sharedChannelRemote = existingScr
|
||||
} else {
|
||||
// the shared channel remote doesn't exists, so we create it
|
||||
scr := &model.SharedChannelRemote{
|
||||
@@ -163,15 +166,26 @@ func (scs *Service) SendChannelInvite(channel *model.Channel, userId string, rc
|
||||
IsInviteConfirmed: true,
|
||||
LastPostCreateAt: curTime,
|
||||
LastPostUpdateAt: curTime,
|
||||
LastMembersSyncAt: 0,
|
||||
}
|
||||
if _, err = scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil {
|
||||
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("Error confirming channel invite for %s: %v", rc.DisplayName, err))
|
||||
return
|
||||
}
|
||||
sharedChannelRemote = scr
|
||||
}
|
||||
|
||||
scs.NotifyChannelChanged(sc.ChannelId)
|
||||
scs.sendEphemeralPost(channel.Id, userId, fmt.Sprintf("`%s` has been added to channel.", rc.DisplayName))
|
||||
|
||||
// Sync all channel members to the remote now that the remote entry exists
|
||||
if syncErr := scs.SyncAllChannelMembers(sc.ChannelId, rc.RemoteId, sharedChannelRemote); syncErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to sync channel members after invite confirmation",
|
||||
mlog.String("channel_id", sc.ChannelId),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
mlog.Err(syncErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if rc.IsPlugin() {
|
||||
@@ -311,6 +325,15 @@ func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model
|
||||
if _, err := scs.server.GetStore().SharedChannel().UpdateRemote(existingScr); err != nil {
|
||||
return fmt.Errorf("cannot restore deleted shared channel remote (channel_id=%s): %w", invite.ChannelId, err)
|
||||
}
|
||||
|
||||
// Sync local channel members to the remote after restoring the shared channel
|
||||
if syncErr := scs.SyncAllChannelMembers(channel.Id, rc.RemoteId, existingScr); syncErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to sync local channel members after restoring shared channel",
|
||||
mlog.String("channel_id", channel.Id),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
mlog.Err(syncErr),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
creatorID := channel.CreatorId
|
||||
if creatorID == "" {
|
||||
@@ -325,6 +348,7 @@ func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model
|
||||
RemoteId: rc.RemoteId,
|
||||
LastPostCreateAt: model.GetMillis(),
|
||||
LastPostUpdateAt: model.GetMillis(),
|
||||
LastMembersSyncAt: 0,
|
||||
}
|
||||
|
||||
if _, err := scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil {
|
||||
@@ -336,6 +360,15 @@ func (scs *Service) onReceiveChannelInvite(msg model.RemoteClusterMsg, rc *model
|
||||
scs.server.GetStore().SharedChannel().Delete(sharedChannel.ChannelId)
|
||||
return fmt.Errorf("cannot create shared channel remote (channel_id=%s): %w", invite.ChannelId, err)
|
||||
}
|
||||
|
||||
// Sync local channel members to the remote after accepting the invitation
|
||||
if syncErr := scs.SyncAllChannelMembers(channel.Id, rc.RemoteId, scr); syncErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to sync local channel members after accepting invitation",
|
||||
mlog.String("channel_id", channel.Id),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
mlog.Err(syncErr),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,18 @@ var (
|
||||
mockTypeContext = mock.MatchedBy(func(ctx context.Context) bool { return true })
|
||||
)
|
||||
|
||||
// setupMockServerWithConfig sets up the standard mocks that all tests need
|
||||
func setupMockServerWithConfig(mockServer *MockServerIface) {
|
||||
// Mock Config for feature flag check - disable membership sync to avoid complex mocking
|
||||
mockConfig := model.Config{}
|
||||
mockConfig.SetDefaults()
|
||||
mockConfig.FeatureFlags.EnableSharedChannelsMemberSync = false
|
||||
mockServer.On("Config").Return(&mockConfig)
|
||||
|
||||
// Mock GetRemoteClusterService for feature flag check
|
||||
mockServer.On("GetRemoteClusterService").Return(nil)
|
||||
}
|
||||
|
||||
func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
t.Run("when msg payload is empty, it does nothing", func(t *testing.T) {
|
||||
mockServer := &MockServerIface{}
|
||||
@@ -92,6 +104,8 @@ func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
mockStore.On("SharedChannel").Return(&mockSharedChannelStore)
|
||||
|
||||
mockServer.On("GetStore").Return(mockStore)
|
||||
setupMockServerWithConfig(mockServer)
|
||||
|
||||
createPostPermission := model.ChannelModeratedPermissionsMap[model.PermissionCreatePost.Id]
|
||||
createReactionPermission := model.ChannelModeratedPermissionsMap[model.PermissionAddReaction.Id]
|
||||
updateMap := model.ChannelModeratedRolesPatch{
|
||||
@@ -216,6 +230,8 @@ func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
mockStore.On("SharedChannel").Return(&mockSharedChannelStore)
|
||||
|
||||
mockServer.On("GetStore").Return(mockStore)
|
||||
setupMockServerWithConfig(mockServer)
|
||||
|
||||
defer mockApp.AssertExpectations(t)
|
||||
|
||||
err = scs.onReceiveChannelInvite(msg, remoteCluster, nil)
|
||||
@@ -351,6 +367,7 @@ func TestOnReceiveChannelInvite(t *testing.T) {
|
||||
|
||||
mockServer = scs.server.(*MockServerIface)
|
||||
mockServer.On("GetStore").Return(mockStore)
|
||||
setupMockServerWithConfig(mockServer)
|
||||
|
||||
mockApp.On("GetOrCreateDirectChannel", mockTypeReqContext, mockTypeString, mockTypeString, mock.AnythingOfType("model.ChannelOption")).
|
||||
Return(channel, nil).Maybe()
|
||||
|
||||
409
server/platform/services/sharedchannel/membership.go
Обычный файл
409
server/platform/services/sharedchannel/membership.go
Обычный файл
@@ -0,0 +1,409 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sharedchannel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/remotecluster"
|
||||
)
|
||||
|
||||
// isChannelMemberSyncEnabled checks if the feature flag is enabled and remote cluster service is available
|
||||
func (scs *Service) isChannelMemberSyncEnabled() bool {
|
||||
featureFlagEnabled := scs.server.Config().FeatureFlags.EnableSharedChannelsMemberSync
|
||||
remoteClusterService := scs.server.GetRemoteClusterService()
|
||||
return featureFlagEnabled && remoteClusterService != nil
|
||||
}
|
||||
|
||||
// queueMembershipSyncTask creates and queues a task to synchronize channel membership changes
|
||||
func (scs *Service) queueMembershipSyncTask(channelID, userID, remoteID string, syncMsg *model.SyncMsg, retryMsg *model.SyncMsg) {
|
||||
task := newSyncTask(channelID, userID, remoteID, syncMsg, retryMsg)
|
||||
task.schedule = time.Now().Add(NotifyMinimumDelay)
|
||||
|
||||
scs.addTask(task)
|
||||
}
|
||||
|
||||
// HandleMembershipChange is called when users are added or removed from a shared channel.
|
||||
// It creates a task to notify all remote clusters about the membership change.
|
||||
func (scs *Service) HandleMembershipChange(channelID, userID string, isAdd bool, remoteID string) {
|
||||
if !scs.isChannelMemberSyncEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
// Create timestamp for consistent usage
|
||||
changeTime := model.GetMillis()
|
||||
|
||||
// Create membership change info
|
||||
syncMsg := model.NewSyncMsg(channelID)
|
||||
syncMsg.MembershipChanges = []*model.MembershipChangeMsg{
|
||||
{
|
||||
ChannelId: channelID,
|
||||
UserId: userID,
|
||||
IsAdd: isAdd,
|
||||
RemoteId: remoteID, // which remote initiated this change
|
||||
ChangeTime: changeTime,
|
||||
},
|
||||
}
|
||||
|
||||
// Queue the membership change task
|
||||
scs.queueMembershipSyncTask(channelID, userID, "", syncMsg, nil)
|
||||
}
|
||||
|
||||
// HandleMembershipBatchChange is called to process a batch of membership changes for a shared channel.
|
||||
// It creates a task to notify all remote clusters about the batch membership changes.
|
||||
func (scs *Service) HandleMembershipBatchChange(channelID string, userIDs []string, isAdd bool, remoteID string) {
|
||||
if !scs.isChannelMemberSyncEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(userIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Create timestamp for consistent usage
|
||||
changeTime := model.GetMillis()
|
||||
|
||||
// Create sync message with membership changes
|
||||
syncMsg := model.NewSyncMsg(channelID)
|
||||
syncMsg.MembershipChanges = make([]*model.MembershipChangeMsg, 0, len(userIDs))
|
||||
|
||||
// Add each user to the batch
|
||||
for _, userID := range userIDs {
|
||||
syncMsg.MembershipChanges = append(syncMsg.MembershipChanges, &model.MembershipChangeMsg{
|
||||
ChannelId: channelID,
|
||||
UserId: userID,
|
||||
IsAdd: isAdd,
|
||||
RemoteId: remoteID,
|
||||
ChangeTime: changeTime,
|
||||
})
|
||||
}
|
||||
|
||||
// Queue the batch membership sync task
|
||||
scs.queueMembershipSyncTask(channelID, "", "", syncMsg, nil)
|
||||
}
|
||||
|
||||
// SyncAllChannelMembers synchronizes all channel members to a specific remote.
|
||||
// This is typically called when a channel is first shared with a remote cluster.
|
||||
// If remote is provided, it will be used instead of fetching from the database.
|
||||
func (scs *Service) SyncAllChannelMembers(channelID string, remoteID string, remote *model.SharedChannelRemote) error {
|
||||
if !scs.isChannelMemberSyncEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify the channel exists and is shared
|
||||
if _, err := scs.server.GetStore().SharedChannel().Get(channelID); err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Failed to get shared channel",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return fmt.Errorf("failed to get shared channel %s: %w", channelID, err)
|
||||
}
|
||||
|
||||
// Get the remote to ensure it exists (if not provided)
|
||||
if remote == nil {
|
||||
var err error
|
||||
remote, err = scs.server.GetStore().SharedChannel().GetRemoteByIds(channelID, remoteID)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Failed to get remote",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return fmt.Errorf("failed to get remote for channel %s: %w", channelID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Use offset-based pagination to handle channels with many members
|
||||
// This ensures we don't skip members when multiple members have the same LastUpdateAt timestamp
|
||||
maxPerPage := scs.GetMemberSyncBatchSize()
|
||||
var allMembers model.ChannelMembers
|
||||
lastSyncAt := remote.LastMembersSyncAt
|
||||
offset := 0
|
||||
|
||||
// Process members incrementally with offset-based pagination
|
||||
for {
|
||||
opts := model.ChannelMembersGetOptions{
|
||||
ChannelID: channelID,
|
||||
UpdatedAfter: lastSyncAt,
|
||||
Limit: maxPerPage,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
members, err1 := scs.server.GetStore().Channel().GetMembers(opts)
|
||||
if err1 != nil {
|
||||
return fmt.Errorf("failed to get members for channel %s: %w", channelID, err1)
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
break // No more members to process
|
||||
}
|
||||
|
||||
// Add to our collection
|
||||
allMembers = append(allMembers, members...)
|
||||
|
||||
// Log progress when processing large channels
|
||||
if len(allMembers)%1000 == 0 {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Processing channel members in batches",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
mlog.Int("processed_so_far", len(allMembers)),
|
||||
)
|
||||
}
|
||||
|
||||
if len(members) < maxPerPage {
|
||||
break // Last page
|
||||
}
|
||||
|
||||
// Move to next page
|
||||
offset += maxPerPage
|
||||
}
|
||||
|
||||
if len(allMembers) == 0 {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "No members to sync for channel",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Syncing all channel members",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
mlog.Int("member_count", len(allMembers)),
|
||||
)
|
||||
|
||||
// Get batch size from config
|
||||
batchSize := scs.GetMemberSyncBatchSize()
|
||||
|
||||
// For small channels, queue individual membership changes
|
||||
if len(allMembers) <= batchSize {
|
||||
return scs.syncMembersIndividually(channelID, remoteID, allMembers, remote)
|
||||
}
|
||||
|
||||
// For larger channels, use batch processing
|
||||
return scs.syncMembersInBatches(channelID, remoteID, allMembers, remote)
|
||||
}
|
||||
|
||||
// syncMembersIndividually processes each member individually
|
||||
// This is more efficient for small channels
|
||||
func (scs *Service) syncMembersIndividually(channelID, remoteID string, members model.ChannelMembers, remote *model.SharedChannelRemote) error {
|
||||
// Queue individual membership changes for each member
|
||||
for _, member := range members {
|
||||
// Queue membership change for this user (isAdd=true)
|
||||
scs.HandleMembershipChange(channelID, member.UserId, true, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncMembersInBatches processes members in batches for greater efficiency
|
||||
// This is better for channels with many members
|
||||
func (scs *Service) syncMembersInBatches(channelID, remoteID string, members model.ChannelMembers, remote *model.SharedChannelRemote) error {
|
||||
// Get batch size from config
|
||||
batchSize := scs.GetMemberSyncBatchSize()
|
||||
|
||||
for i := 0; i < len(members); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(members) {
|
||||
end = len(members)
|
||||
}
|
||||
|
||||
// Create a batch of members
|
||||
batchMembers := members[i:end]
|
||||
|
||||
// Extract user IDs from the batch
|
||||
userIDs := make([]string, len(batchMembers))
|
||||
for j, member := range batchMembers {
|
||||
userIDs[j] = member.UserId
|
||||
}
|
||||
|
||||
// Use the batch handling function to queue the changes
|
||||
scs.HandleMembershipBatchChange(channelID, userIDs, true, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processMembershipChange processes a channel membership change task.
|
||||
// It determines which remotes should receive the update and creates tasks for each.
|
||||
func (scs *Service) processMembershipChange(syncMsg *model.SyncMsg) {
|
||||
if len(syncMsg.MembershipChanges) == 0 {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Invalid membership change task - no membership changes",
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the shared channel (to verify it exists)
|
||||
_, err := scs.server.GetStore().SharedChannel().Get(syncMsg.ChannelId)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to get shared channel for membership change",
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Int("change_count", len(syncMsg.MembershipChanges)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Get all remotes for this channel
|
||||
remotes, err := scs.server.GetStore().SharedChannel().GetRemotes(0, 999999, model.SharedChannelRemoteFilterOpts{
|
||||
ChannelId: syncMsg.ChannelId,
|
||||
})
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to get shared channel remotes for membership change",
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Always use batch processing for consistency (works for single or multiple changes)
|
||||
scs.syncMembershipBatchToRemotes(syncMsg, remotes)
|
||||
}
|
||||
|
||||
// syncMembershipBatchToRemotes synchronizes membership changes (single or batch) with remote clusters.
|
||||
func (scs *Service) syncMembershipBatchToRemotes(syncMsg *model.SyncMsg, remotes []*model.SharedChannelRemote) {
|
||||
if len(syncMsg.MembershipChanges) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the initiating remote ID from the first change (all should be the same)
|
||||
initiatingRemoteId := ""
|
||||
if len(syncMsg.MembershipChanges) > 0 {
|
||||
initiatingRemoteId = syncMsg.MembershipChanges[0].RemoteId
|
||||
}
|
||||
|
||||
// Send to all remotes except the one that initiated this change
|
||||
for _, remote := range remotes {
|
||||
// Skip the remote that initiated this change to prevent loops
|
||||
if remote.RemoteId == initiatingRemoteId {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the remote cluster
|
||||
rc, err := scs.server.GetStore().RemoteCluster().Get(remote.RemoteId, false)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to get remote cluster for batch membership sync",
|
||||
mlog.String("remote_id", remote.RemoteId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a copy of the sync message to potentially add user profiles
|
||||
enrichedSyncMsg := &model.SyncMsg{
|
||||
Id: syncMsg.Id,
|
||||
ChannelId: syncMsg.ChannelId,
|
||||
MembershipChanges: syncMsg.MembershipChanges,
|
||||
Users: make(map[string]*model.User),
|
||||
}
|
||||
|
||||
// Add user profiles for all users being added
|
||||
for _, change := range syncMsg.MembershipChanges {
|
||||
if change.IsAdd {
|
||||
user, pErr := scs.server.GetStore().User().Get(context.Background(), change.UserId)
|
||||
if pErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Failed to get user for batch membership sync",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.String("remote_id", remote.RemoteId),
|
||||
mlog.Err(pErr),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if user profile needs to be synced
|
||||
doSync, _, sErr := scs.shouldUserSync(user, syncMsg.ChannelId, rc)
|
||||
if sErr == nil && doSync {
|
||||
enrichedSyncMsg.Users[user.Id] = user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send message using the existing remote cluster framework
|
||||
payload, err := json.Marshal(enrichedSyncMsg)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to marshal batch membership message",
|
||||
mlog.String("remote_id", remote.RemoteId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
msg := model.RemoteClusterMsg{
|
||||
Id: model.NewId(),
|
||||
Topic: TopicChannelMembership,
|
||||
CreateAt: model.GetMillis(),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), remotecluster.SendTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Define a callback function
|
||||
callback := func(msg model.RemoteClusterMsg, rc *model.RemoteCluster, resp *remotecluster.Response, err error) {
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error sending batch membership changes to remote",
|
||||
mlog.String("remote", remote.RemoteId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Int("change_count", len(syncMsg.MembershipChanges)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if resp != nil && resp.Err != "" {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Remote error when processing batch membership changes",
|
||||
mlog.String("remote", remote.RemoteId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.String("remote_error", resp.Err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Update sync timestamps
|
||||
for _, change := range syncMsg.MembershipChanges {
|
||||
if err := scs.server.GetStore().SharedChannel().UpdateUserLastMembershipSyncAt(change.UserId, change.ChannelId, remote.RemoteId, change.ChangeTime); err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to update user membership sync timestamp in batch",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the cursor with the latest change time
|
||||
var maxChangeTime int64
|
||||
for _, change := range syncMsg.MembershipChanges {
|
||||
if change.ChangeTime > maxChangeTime {
|
||||
maxChangeTime = change.ChangeTime
|
||||
}
|
||||
}
|
||||
|
||||
if err := scs.updateMembershipSyncCursor(syncMsg.ChannelId, remote.RemoteId, maxChangeTime); err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to update membership sync cursor for batch",
|
||||
mlog.String("remote_id", remote.RemoteId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
err = scs.server.GetRemoteClusterService().SendMsg(ctx, msg, rc, callback)
|
||||
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to send batch membership changes to remote",
|
||||
mlog.String("remote_id", remote.RemoteId),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
213
server/platform/services/sharedchannel/membership_recv.go
Обычный файл
213
server/platform/services/sharedchannel/membership_recv.go
Обычный файл
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sharedchannel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/remotecluster"
|
||||
)
|
||||
|
||||
// checkMembershipConflict checks if there are newer changes that would conflict with this one
|
||||
// Returns true if this change should be skipped due to a conflict
|
||||
func (scs *Service) checkMembershipConflict(userID, channelID string, changeTime int64) (bool, error) {
|
||||
conflicts, err := scs.server.GetStore().SharedChannel().GetUserChanges(userID, channelID, changeTime)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to check for membership change conflicts",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If there are conflicting operations, the latest one wins
|
||||
for _, conflict := range conflicts {
|
||||
if conflict.LastMembershipSyncAt > changeTime {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Ignoring older membership change due to conflict",
|
||||
mlog.String("user_id", userID),
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.Int("change_time", int(changeTime)),
|
||||
mlog.Int("conflicting_time", int(conflict.LastMembershipSyncAt)),
|
||||
)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// onReceiveMembershipChanges processes channel membership changes from a remote cluster
|
||||
func (scs *Service) onReceiveMembershipChanges(syncMsg *model.SyncMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
|
||||
// Check if feature flag is enabled
|
||||
if !scs.server.Config().FeatureFlags.EnableSharedChannelsMemberSync {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(syncMsg.MembershipChanges) == 0 {
|
||||
return fmt.Errorf("onReceiveMembershipChanges: no membership changes")
|
||||
}
|
||||
|
||||
// Get the channel to make sure it exists and is shared
|
||||
channel, err := scs.server.GetStore().Channel().Get(syncMsg.ChannelId, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get channel for membership changes: %w", err)
|
||||
}
|
||||
|
||||
// Verify this is a valid shared channel
|
||||
_, err = scs.server.GetStore().SharedChannel().Get(syncMsg.ChannelId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get shared channel for membership changes: %w", err)
|
||||
}
|
||||
|
||||
// Calculate the maximum ChangeTime from all changes in the batch
|
||||
var maxChangeTime int64
|
||||
for _, change := range syncMsg.MembershipChanges {
|
||||
if change.ChangeTime > maxChangeTime {
|
||||
maxChangeTime = change.ChangeTime
|
||||
}
|
||||
}
|
||||
|
||||
// Process each change
|
||||
var successCount, skipCount, failCount int
|
||||
|
||||
for _, change := range syncMsg.MembershipChanges {
|
||||
// Check for conflicts
|
||||
shouldSkip, _ := scs.checkMembershipConflict(change.UserId, change.ChannelId, change.ChangeTime)
|
||||
if shouldSkip {
|
||||
skipCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the membership change based on whether it's an add or remove
|
||||
var processErr error
|
||||
if change.IsAdd {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Adding user to channel from remote cluster",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
)
|
||||
processErr = scs.processMemberAdd(change, channel, rc, maxChangeTime)
|
||||
} else {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Removing user from channel from remote cluster",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
)
|
||||
processErr = scs.processMemberRemove(change, rc, maxChangeTime)
|
||||
}
|
||||
|
||||
if processErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to process membership change",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
mlog.Bool("is_add", change.IsAdd),
|
||||
mlog.Err(processErr),
|
||||
)
|
||||
failCount++
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processMemberAdd handles adding a user to a channel as part of batch processing
|
||||
func (scs *Service) processMemberAdd(change *model.MembershipChangeMsg, channel *model.Channel, rc *model.RemoteCluster, maxChangeTime int64) error {
|
||||
// Get the user if they exist
|
||||
user, err := scs.server.GetStore().User().Get(request.EmptyContext(scs.server.Log()).Context(), change.UserId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get user for channel add: %w", err)
|
||||
}
|
||||
|
||||
// Check user permissions for private channels
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
// Add user to team if needed for private channel
|
||||
rctx := request.EmptyContext(scs.server.Log())
|
||||
appErr := scs.app.AddUserToTeamByTeamId(rctx, channel.TeamId, user)
|
||||
if appErr != nil {
|
||||
return fmt.Errorf("cannot add user to team for private channel: %w", appErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the app layer to add the user to the channel
|
||||
// This ensures proper processing of all side effects
|
||||
rctx := request.EmptyContext(scs.server.Log())
|
||||
_, appErr := scs.app.AddUserToChannel(rctx, user, channel, false)
|
||||
if appErr != nil {
|
||||
// Skip "already added" errors
|
||||
if appErr.Error() != "api.channel.add_user.to_channel.failed.app_error" &&
|
||||
!strings.Contains(appErr.Error(), "channel_member_exists") {
|
||||
return fmt.Errorf("cannot add user to channel: %w", appErr)
|
||||
}
|
||||
// User is already in the channel, which is fine
|
||||
}
|
||||
|
||||
// Update the sync status
|
||||
if syncErr := scs.server.GetStore().SharedChannel().UpdateUserLastMembershipSyncAt(change.UserId, change.ChannelId, rc.RemoteId, maxChangeTime); syncErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to update user LastMembershipSyncAt after batch member add",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
mlog.Err(syncErr),
|
||||
)
|
||||
// Continue despite the error - this is not critical
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processMemberRemove handles removing a user from a channel as part of batch processing
|
||||
func (scs *Service) processMemberRemove(change *model.MembershipChangeMsg, rc *model.RemoteCluster, maxChangeTime int64) error {
|
||||
// Get channel so we can use app layer methods properly
|
||||
channel, err := scs.server.GetStore().Channel().Get(change.ChannelId, true)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Cannot find channel for member removal",
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.Err(err),
|
||||
)
|
||||
// Continue anyway to update sync status - the channel might be deleted
|
||||
}
|
||||
|
||||
// Use the app layer's remove user method if channel still exists
|
||||
if channel != nil {
|
||||
rctx := request.EmptyContext(scs.server.Log())
|
||||
// We use empty string for removerUserId to indicate system-initiated removal
|
||||
// This also ensures we bypass permission checks intended for user-initiated removals
|
||||
appErr := scs.app.RemoveUserFromChannel(rctx, change.UserId, "", channel)
|
||||
if appErr != nil {
|
||||
// Ignore "not found" errors - the user might already be removed
|
||||
if !strings.Contains(appErr.Error(), "store.sql_channel.remove_member.missing.app_error") {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceWarn, "Error removing user from channel",
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.Err(appErr),
|
||||
)
|
||||
// Continue anyway to update sync status - don't return error here
|
||||
// to ensure sync status still gets updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the sync status
|
||||
if syncErr := scs.server.GetStore().SharedChannel().UpdateUserLastMembershipSyncAt(change.UserId, change.ChannelId, rc.RemoteId, maxChangeTime); syncErr != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to update user LastMembershipSyncAt after batch member remove",
|
||||
mlog.String("user_id", change.UserId),
|
||||
mlog.String("channel_id", change.ChannelId),
|
||||
mlog.String("remote_id", rc.RemoteId),
|
||||
mlog.Err(syncErr),
|
||||
)
|
||||
// Continue despite the error - this is not critical
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -513,6 +513,26 @@ func (_m *MockAppIface) Publish(message *model.WebSocketEvent) {
|
||||
_m.Called(message)
|
||||
}
|
||||
|
||||
// RemoveUserFromChannel provides a mock function with given fields: c, userID, removerUserId, channel
|
||||
func (_m *MockAppIface) RemoveUserFromChannel(c request.CTX, userID string, removerUserId string, channel *model.Channel) *model.AppError {
|
||||
ret := _m.Called(c, userID, removerUserId, channel)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveUserFromChannel")
|
||||
}
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(request.CTX, string, string, *model.Channel) *model.AppError); ok {
|
||||
r0 = rf(c, userID, removerUserId, channel)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SaveAndBroadcastStatus provides a mock function with given fields: status
|
||||
func (_m *MockAppIface) SaveAndBroadcastStatus(status *model.Status) {
|
||||
_m.Called(status)
|
||||
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
TopicSync = "sharedchannel_sync"
|
||||
TopicChannelInvite = "sharedchannel_invite"
|
||||
TopicUploadCreate = "sharedchannel_upload"
|
||||
TopicChannelMembership = "sharedchannel_membership"
|
||||
TopicGlobalUserSync = "sharedchannel_global_user_sync"
|
||||
MaxRetries = 3
|
||||
MaxUsersPerSync = 25
|
||||
@@ -31,6 +32,7 @@ const (
|
||||
NotifyMinimumDelay = time.Second * 2
|
||||
MaxUpsertRetries = 25
|
||||
ProfileImageSyncTimeout = time.Second * 5
|
||||
// Default value for MaxMembersPerBatch is defined in config.go as ConnectedWorkspacesSettingsDefaultMemberSyncBatchSize
|
||||
)
|
||||
|
||||
// Mocks can be re-generated with `make sharedchannel-mocks`.
|
||||
@@ -58,6 +60,7 @@ type AppIface interface {
|
||||
UserCanSeeOtherUser(c request.CTX, userID string, otherUserId string) (bool, *model.AppError)
|
||||
AddUserToChannel(c request.CTX, user *model.User, channel *model.Channel, skipTeamMemberIntegrityCheck bool) (*model.ChannelMember, *model.AppError)
|
||||
AddUserToTeamByTeamId(c request.CTX, teamId string, user *model.User) *model.AppError
|
||||
RemoveUserFromChannel(c request.CTX, userID string, removerUserId string, channel *model.Channel) *model.AppError
|
||||
PermanentDeleteChannel(c request.CTX, channel *model.Channel) *model.AppError
|
||||
CreatePost(c request.CTX, post *model.Post, channel *model.Channel, flags model.CreatePostFlags) (savedPost *model.Post, err *model.AppError)
|
||||
UpdatePost(c request.CTX, post *model.Post, updatePostOptions *model.UpdatePostOptions) (*model.Post, *model.AppError)
|
||||
@@ -90,9 +93,10 @@ type Service struct {
|
||||
changeSignal chan struct{}
|
||||
|
||||
// everything below guarded by `mux`
|
||||
mux sync.RWMutex
|
||||
active bool
|
||||
leaderListenerId string
|
||||
mux sync.RWMutex
|
||||
active bool
|
||||
leaderListenerId string
|
||||
|
||||
connectionStateListenerId string
|
||||
done chan struct{}
|
||||
tasks map[string]syncTask
|
||||
@@ -136,6 +140,8 @@ func (scs *Service) Start() error {
|
||||
scs.connectionStateListenerId = rcs.AddConnectionStateListener(scs.onConnectionStateChange)
|
||||
scs.mux.Unlock()
|
||||
|
||||
rcs.AddTopicListener(TopicChannelMembership, scs.onReceiveSyncMessage)
|
||||
|
||||
scs.onClusterLeaderChange()
|
||||
|
||||
return nil
|
||||
@@ -220,6 +226,14 @@ func (scs *Service) pause() {
|
||||
scs.server.Log().Debug("Shared Channel Service inactive")
|
||||
}
|
||||
|
||||
// GetMemberSyncBatchSize returns the configured batch size for member synchronization
|
||||
func (scs *Service) GetMemberSyncBatchSize() int {
|
||||
if scs.server.Config().ConnectedWorkspacesSettings.MemberSyncBatchSize != nil {
|
||||
return *scs.server.Config().ConnectedWorkspacesSettings.MemberSyncBatchSize
|
||||
}
|
||||
return model.ConnectedWorkspacesSettingsDefaultMemberSyncBatchSize
|
||||
}
|
||||
|
||||
// Makes the remote channel to be read-only(announcement mode, only admins can create posts and reactions).
|
||||
func (scs *Service) makeChannelReadOnly(channel *model.Channel) *model.AppError {
|
||||
createPostPermission := model.ChannelModeratedPermissionsMap[model.PermissionCreatePost.Id]
|
||||
@@ -314,17 +328,19 @@ func (scs *Service) scheduleGlobalUserSync(rc *model.RemoteCluster) {
|
||||
}()
|
||||
}
|
||||
|
||||
// OnReceiveSyncMessageForTesting exposes onReceiveSyncMessage for testing
|
||||
func (scs *Service) OnReceiveSyncMessageForTesting(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
|
||||
return scs.onReceiveSyncMessage(msg, rc, response)
|
||||
}
|
||||
|
||||
// GetUserSyncBatchSizeForTesting returns the configured batch size for user syncing (exported for testing)
|
||||
func (scs *Service) GetUserSyncBatchSizeForTesting() int {
|
||||
return scs.getGlobalUserSyncBatchSize()
|
||||
// HasPendingTasksForTesting returns true if there are pending sync tasks in the queue
|
||||
func (scs *Service) HasPendingTasksForTesting() bool {
|
||||
scs.mux.RLock()
|
||||
defer scs.mux.RUnlock()
|
||||
return len(scs.tasks) > 0
|
||||
}
|
||||
|
||||
// HandleSyncAllUsersForTesting exposes syncAllUsers for testing
|
||||
func (scs *Service) HandleSyncAllUsersForTesting(rc *model.RemoteCluster) error {
|
||||
return scs.syncAllUsers(rc)
|
||||
}
|
||||
|
||||
// OnReceiveSyncMessageForTesting exposes onReceiveSyncMessage for testing
|
||||
func (scs *Service) OnReceiveSyncMessageForTesting(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
|
||||
return scs.onReceiveSyncMessage(msg, rc, response)
|
||||
}
|
||||
|
||||
@@ -277,3 +277,41 @@ func (scs *Service) CheckCanInviteToSharedChannel(channelId string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateMembershipSyncCursor updates the LastMembersSyncAt value for the shared channel remote
|
||||
// This provides centralized and consistent cursor management
|
||||
func (scs *Service) updateMembershipSyncCursor(channelID string, remoteID string, newTimestamp int64) error {
|
||||
// Get the remote record
|
||||
scr, err := scs.server.GetStore().SharedChannel().GetRemoteByIds(channelID, remoteID)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to get shared channel remote for cursor update",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
mlog.Int("timestamp", int(newTimestamp)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return fmt.Errorf("failed to get shared channel remote for cursor update: %w", err)
|
||||
}
|
||||
|
||||
if scr == nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Shared channel remote not found for cursor update",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
)
|
||||
return fmt.Errorf("shared channel remote not found for channel %s and remote %s", channelID, remoteID)
|
||||
}
|
||||
|
||||
// Update the cursor - the store will handle ensuring it only moves forward
|
||||
err = scs.server.GetStore().SharedChannel().UpdateRemoteMembershipCursor(scr.Id, newTimestamp)
|
||||
if err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Failed to update membership cursor",
|
||||
mlog.String("channel_id", channelID),
|
||||
mlog.String("remote_id", remoteID),
|
||||
mlog.String("remote_record_id", scr.Id),
|
||||
mlog.Int("timestamp", int(newTimestamp)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,10 +24,9 @@ var (
|
||||
)
|
||||
|
||||
func (scs *Service) onReceiveSyncMessage(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
|
||||
if msg.Topic != TopicSync && msg.Topic != TopicGlobalUserSync {
|
||||
return fmt.Errorf("wrong topic, expected `%s` or `%s`, got `%s`", TopicSync, TopicGlobalUserSync, msg.Topic)
|
||||
if msg.Topic != TopicSync && msg.Topic != TopicChannelMembership && msg.Topic != TopicGlobalUserSync {
|
||||
return fmt.Errorf("wrong topic, expected sync-related topic, got `%s`", msg.Topic)
|
||||
}
|
||||
|
||||
if len(msg.Payload) == 0 {
|
||||
return errors.New("empty sync message")
|
||||
}
|
||||
@@ -89,6 +88,15 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
|
||||
ReactionErrors: make([]string, 0),
|
||||
}
|
||||
|
||||
// Check if feature flag is enabled for membership changes
|
||||
membershipSyncEnabled := scs.server.Config().FeatureFlags.EnableSharedChannelsMemberSync
|
||||
hasMembershipChanges := len(syncMsg.MembershipChanges) > 0
|
||||
|
||||
// If this message only contains membership changes and feature is disabled, skip it
|
||||
if hasMembershipChanges && !membershipSyncEnabled && len(syncMsg.Users) == 0 && len(syncMsg.Posts) == 0 && len(syncMsg.Reactions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Sync msg received",
|
||||
mlog.String("remote", rc.Name),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
@@ -96,6 +104,7 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
|
||||
mlog.Int("post_count", len(syncMsg.Posts)),
|
||||
mlog.Int("reaction_count", len(syncMsg.Reactions)),
|
||||
mlog.Int("status_count", len(syncMsg.Statuses)),
|
||||
mlog.Int("membership_change_count", len(syncMsg.MembershipChanges)),
|
||||
)
|
||||
|
||||
// Check if this is a global user sync message (no channel ID and only users)
|
||||
@@ -229,6 +238,19 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
|
||||
scs.app.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
// Process membership changes after users have been synced
|
||||
if hasMembershipChanges && membershipSyncEnabled {
|
||||
if err := scs.onReceiveMembershipChanges(syncMsg, rc, response); err != nil {
|
||||
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error processing membership changes",
|
||||
mlog.String("remote", rc.Name),
|
||||
mlog.String("channel_id", syncMsg.ChannelId),
|
||||
mlog.Int("change_count", len(syncMsg.MembershipChanges)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
// Don't fail the entire sync if membership changes fail
|
||||
}
|
||||
}
|
||||
|
||||
response.SetPayload(syncResp)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -37,8 +37,17 @@ func newSyncTask(channelID, userID string, remoteID string, existingMsg, retryMs
|
||||
retryID = retryMsg.Id
|
||||
}
|
||||
|
||||
// Generate a unique task ID
|
||||
taskID := channelID + userID + remoteID + retryID // combination of ids to avoid duplicates
|
||||
|
||||
// For batch tasks, add a batch identifier to make the ID unique
|
||||
if existingMsg != nil && len(existingMsg.MembershipChanges) > 1 {
|
||||
batchID := model.NewId()[:8] // Use a short unique ID for the batch
|
||||
taskID = channelID + "batch" + batchID + remoteID + retryID
|
||||
}
|
||||
|
||||
return syncTask{
|
||||
id: channelID + userID + remoteID + retryID, // combination of ids to avoid duplicates
|
||||
id: taskID,
|
||||
channelID: channelID,
|
||||
userID: userID,
|
||||
remoteID: remoteID, // empty means update all remote clusters
|
||||
@@ -235,6 +244,7 @@ func (scs *Service) ForceSyncForRemote(rc *model.RemoteCluster) {
|
||||
// addTask adds or re-adds a task to the queue.
|
||||
func (scs *Service) addTask(task syncTask) {
|
||||
task.AddedAt = time.Now()
|
||||
|
||||
scs.mux.Lock()
|
||||
if originalTask, ok := scs.tasks[task.id]; ok {
|
||||
// if the task was already scheduled, we only update the
|
||||
@@ -365,6 +375,16 @@ func (scs *Service) removeOldestTask() (syncTask, bool, time.Duration) {
|
||||
|
||||
// processTask updates one or more remote clusters with any new channel content.
|
||||
func (scs *Service) processTask(task syncTask) error {
|
||||
// Check if this is a membership change task
|
||||
if task.existingMsg != nil && len(task.existingMsg.MembershipChanges) > 0 {
|
||||
// Check if feature flag is enabled
|
||||
if !scs.server.Config().FeatureFlags.EnableSharedChannelsMemberSync {
|
||||
return nil
|
||||
}
|
||||
scs.processMembershipChange(task.existingMsg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// map is used to ensure remotes don't get sync'd twice, such as when
|
||||
// they have the autoinvited flag and have explicitly subscribed to a channel.
|
||||
remotesMap := make(map[string]*model.RemoteCluster)
|
||||
|
||||
@@ -119,6 +119,7 @@ func (scs *Service) syncForRemote(task syncTask, rc *model.RemoteCluster) error
|
||||
RemoteId: rc.RemoteId,
|
||||
LastPostCreateAt: model.GetMillis(),
|
||||
LastPostUpdateAt: model.GetMillis(),
|
||||
LastMembersSyncAt: 0,
|
||||
}
|
||||
if scr, err = scs.server.GetStore().SharedChannel().SaveRemote(scr); err != nil {
|
||||
return fmt.Errorf("cannot auto-create shared channel remote (channel_id=%s, remote_id=%s): %w", task.channelID, rc.RemoteId, err)
|
||||
|
||||
Ссылка в новой задаче
Block a user