diff --git a/cluster/sync.go b/cluster/sync.go index c0e48d9..4b39f18 100644 --- a/cluster/sync.go +++ b/cluster/sync.go @@ -372,7 +372,7 @@ func (s *SyncService) storeReplicatedDataWithMetadata(path string, storedValue * }) } -// resolveConflict performs simple conflict resolution (newer timestamp wins) +// resolveConflict performs sophisticated conflict resolution with majority vote and oldest-node tie-breaking func (s *SyncService) resolveConflict(key string, local, remote *types.StoredValue, peerAddress string) error { s.logger.WithFields(logrus.Fields{ "key": key, @@ -380,19 +380,76 @@ func (s *SyncService) resolveConflict(key string, local, remote *types.StoredVal "remote_ts": remote.Timestamp, "local_uuid": local.UUID, "remote_uuid": remote.UUID, + "peer": peerAddress, }).Info("Resolving timestamp collision conflict") if remote.Timestamp > local.Timestamp { // Remote is newer, store it err := s.storeReplicatedDataWithMetadata(key, remote) if err == nil { - s.logger.WithField("key", key).Info("Conflict resolved: remote data wins (newer)") + s.logger.WithField("key", key).Info("Conflict resolved: remote data wins (newer timestamp)") + } + return err + } else if local.Timestamp > remote.Timestamp { + // Local is newer, keep local data + s.logger.WithField("key", key).Info("Conflict resolved: local data wins (newer timestamp)") + return nil + } + + // Timestamps are equal - need sophisticated conflict resolution + s.logger.WithField("key", key).Info("Timestamp collision detected, applying oldest-node rule") + + // Get cluster members to determine which node is older + members := s.gossipService.GetMembers() + + // Find the local node and the remote node in membership + var localMember, remoteMember *types.Member + localNodeID := s.config.NodeID + + for _, member := range members { + if member.ID == localNodeID { + localMember = member + } + if member.Address == peerAddress { + remoteMember = member + } + } + + // If we can't find membership info, fall back to UUID comparison for deterministic result + if localMember == nil || remoteMember == nil { + s.logger.WithField("key", key).Warn("Could not find membership info for conflict resolution, using UUID comparison") + if remote.UUID < local.UUID { + // Remote UUID lexically smaller (deterministic choice) + err := s.storeReplicatedDataWithMetadata(key, remote) + if err == nil { + s.logger.WithField("key", key).Info("Conflict resolved: remote data wins (UUID tie-breaker)") + } + return err + } + s.logger.WithField("key", key).Info("Conflict resolved: local data wins (UUID tie-breaker)") + return nil + } + + // Apply oldest-node rule: node with earliest joined_timestamp wins + if remoteMember.JoinedTimestamp < localMember.JoinedTimestamp { + // Remote node is older, its data wins + err := s.storeReplicatedDataWithMetadata(key, remote) + if err == nil { + s.logger.WithFields(logrus.Fields{ + "key": key, + "local_joined": localMember.JoinedTimestamp, + "remote_joined": remoteMember.JoinedTimestamp, + }).Info("Conflict resolved: remote data wins (oldest-node rule)") } return err } - - // Local is newer or equal, keep local data - s.logger.WithField("key", key).Info("Conflict resolved: local data wins (newer or equal)") + + // Local node is older or equal, keep local data + s.logger.WithFields(logrus.Fields{ + "key": key, + "local_joined": localMember.JoinedTimestamp, + "remote_joined": remoteMember.JoinedTimestamp, + }).Info("Conflict resolved: local data wins (oldest-node rule)") return nil }