blob: a377444684b2e2b877f695aac26eb1b725aea9c4 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package controller
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/apache/kvrocks-controller/consts"
"github.com/apache/kvrocks-controller/store"
"github.com/apache/kvrocks-controller/store/engine"
)
type MockClusterStore struct {
*store.ClusterStore
mu sync.Mutex
clusters map[string]*store.Cluster
}
func NewMockClusterStore() *MockClusterStore {
return &MockClusterStore{
ClusterStore: store.NewClusterStore(engine.NewMock()),
clusters: make(map[string]*store.Cluster),
}
}
func (s *MockClusterStore) CreateCluster(ctx context.Context, ns string, cluster *store.Cluster) error {
s.mu.Lock()
defer s.mu.Unlock()
s.clusters[cluster.Name] = cluster
return nil
}
func (s *MockClusterStore) GetCluster(ctx context.Context, ns, cluster string) (*store.Cluster, error) {
s.mu.Lock()
defer s.mu.Unlock()
c, ok := s.clusters[cluster]
if !ok {
return nil, consts.ErrNotFound
}
return c, nil
}
func (s *MockClusterStore) UpdateCluster(ctx context.Context, ns string, cluster *store.Cluster) error {
cluster.Version.Add(1)
return s.SetCluster(ctx, ns, cluster)
}
func (s *MockClusterStore) SetCluster(ctx context.Context, ns string, cluster *store.Cluster) error {
s.mu.Lock()
defer s.mu.Unlock()
s.clusters[cluster.Name] = cluster
return nil
}
func (s *MockClusterStore) RemoveCluster(ctx context.Context, ns, cluster string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.clusters, cluster)
return nil
}
func TestCluster_FailureCount(t *testing.T) {
ctx := context.Background()
ns := "test-ns"
clusterName := "test-clusterName"
s := NewMockClusterStore()
mockNode0 := store.NewClusterMockNode()
mockNode0.SetRole(store.RoleMaster)
mockNode0.Sequence = 104
mockNode1 := store.NewClusterMockNode()
mockNode1.SetRole(store.RoleSlave)
mockNode1.Sequence = 102
mockNode2 := store.NewClusterMockNode()
mockNode2.SetRole(store.RoleSlave)
mockNode2.Sequence = 103
mockNode3 := store.NewClusterMockNode()
mockNode3.SetRole(store.RoleSlave)
mockNode3.Sequence = 101
clusterInfo := &store.Cluster{
Name: clusterName,
Shards: []*store.Shard{{
Nodes: []store.Node{
mockNode0, mockNode1, mockNode2, mockNode3,
},
SlotRanges: []store.SlotRange{{Start: 0, Stop: 16383}},
MigratingSlot: &store.MigratingSlot{IsMigrating: false},
TargetShardIndex: -1,
}},
}
clusterInfo.Version.Store(1)
require.NoError(t, s.CreateCluster(ctx, ns, clusterInfo))
cluster := &ClusterChecker{
clusterStore: s,
namespace: ns,
clusterName: clusterName,
options: ClusterCheckOptions{
pingInterval: time.Second,
maxFailureCount: 3,
enableSlaveHAUpdate: true,
},
failureCounts: make(map[string]int64),
syncCh: make(chan struct{}, 1),
}
require.EqualValues(t, 1, clusterInfo.Version.Load())
for i := int64(0); i < cluster.options.maxFailureCount-1; i++ {
require.EqualValues(t, i+1, cluster.increaseFailureCount(0, mockNode2))
}
for i := int64(0); i < cluster.options.maxFailureCount; i++ {
require.EqualValues(t, i+1, cluster.increaseFailureCount(0, mockNode0))
}
require.False(t, mockNode0.IsMaster())
// mockNode2 should become the new master since its sequence is the largest
require.True(t, mockNode2.IsMaster())
require.EqualValues(t, 2, clusterInfo.Version.Load())
require.EqualValues(t, 0, cluster.failureCounts[mockNode2.Addr()])
require.True(t, mockNode2.IsMaster())
// Slave failure count keeps increasing; at threshold the slave is auto-marked as failed.
for i := int64(0); i < cluster.options.maxFailureCount*2; i++ {
require.EqualValues(t, i+1, cluster.increaseFailureCount(0, mockNode3))
}
require.True(t, mockNode3.Failed())
require.EqualValues(t, 3, clusterInfo.Version.Load())
cluster.resetFailureCount(mockNode3.ID())
require.EqualValues(t, 0, cluster.failureCounts[mockNode3.ID()])
}
func TestCluster_SlaveFailureAutoOffline(t *testing.T) {
ctx := context.Background()
ns := "test-ns"
clusterName := "test-slave-offline"
s := NewMockClusterStore()
mockMaster := store.NewClusterMockNode()
mockMaster.SetRole(store.RoleMaster)
mockMaster.Sequence = 100
mockSlave1 := store.NewClusterMockNode()
mockSlave1.SetRole(store.RoleSlave)
mockSlave1.Sequence = 90
mockSlave2 := store.NewClusterMockNode()
mockSlave2.SetRole(store.RoleSlave)
mockSlave2.Sequence = 80
clusterInfo := &store.Cluster{
Name: clusterName,
Shards: []*store.Shard{{
Nodes: []store.Node{mockMaster, mockSlave1, mockSlave2},
SlotRanges: []store.SlotRange{{Start: 0, Stop: 16383}},
MigratingSlot: &store.MigratingSlot{IsMigrating: false},
TargetShardIndex: -1,
}},
}
clusterInfo.Version.Store(1)
require.NoError(t, s.CreateCluster(ctx, ns, clusterInfo))
checker := &ClusterChecker{
clusterStore: s,
namespace: ns,
clusterName: clusterName,
options: ClusterCheckOptions{
pingInterval: time.Second,
maxFailureCount: 3,
enableSlaveHAUpdate: true,
},
failureCounts: make(map[string]int64),
syncCh: make(chan struct{}, 1),
}
// Slave should not be marked as failed before reaching threshold
require.False(t, mockSlave1.Failed())
for i := int64(0); i < checker.options.maxFailureCount-1; i++ {
checker.increaseFailureCount(0, mockSlave1)
}
require.False(t, mockSlave1.Failed())
require.EqualValues(t, 1, clusterInfo.Version.Load())
// Slave should be marked as failed when reaching threshold
checker.increaseFailureCount(0, mockSlave1)
require.True(t, mockSlave1.Failed())
require.EqualValues(t, 2, clusterInfo.Version.Load())
// Subsequent failures should not trigger another update (already failed)
checker.increaseFailureCount(0, mockSlave1)
require.True(t, mockSlave1.Failed())
require.EqualValues(t, 2, clusterInfo.Version.Load())
// Other slaves are not affected
require.False(t, mockSlave2.Failed())
// Master should not be affected by slave offline logic
require.True(t, mockMaster.IsMaster())
require.False(t, mockMaster.Failed())
}
func TestCluster_LoadAndProbe(t *testing.T) {
ctx := context.Background()
ns := "test-ns"
clusterName := "test-clusterProbe"
cluster, err := store.NewCluster(clusterName, []string{"127.0.0.1:7770", "127.0.0.1:7771"}, 2)
require.NoError(t, err)
nodes := make([]*store.ClusterNode, 0)
for _, shard := range cluster.Shards {
for _, node := range shard.Nodes {
clusterNode, _ := node.(*store.ClusterNode)
nodes = append(nodes, clusterNode)
}
}
require.NoError(t, cluster.Reset(ctx))
defer func() {
require.NoError(t, cluster.Reset(ctx))
}()
s := NewMockClusterStore()
require.NoError(t, s.CreateCluster(ctx, ns, cluster))
clusterProbe := NewClusterChecker(s, ns, clusterName)
clusterProbe.WithPingInterval(100 * time.Millisecond)
clusterProbe.Start()
defer clusterProbe.Close()
ticker := time.NewTicker(400 * time.Millisecond)
defer ticker.Stop()
<-ticker.C
for _, node := range nodes {
info, err := node.GetClusterInfo(ctx)
require.NoError(t, err)
require.EqualValues(t, 1, info.CurrentEpoch)
}
require.NoError(t, s.UpdateCluster(ctx, ns, cluster))
<-ticker.C
// should sync the clusterName info
for _, node := range nodes {
info, err := node.GetClusterInfo(ctx)
require.NoError(t, err)
require.EqualValues(t, 2, info.CurrentEpoch)
}
require.NoError(t, s.UpdateCluster(ctx, ns, cluster))
clusterProbe.sendSyncEvent()
<-ticker.C
for _, node := range nodes {
info, err := node.GetClusterInfo(ctx)
require.NoError(t, err)
require.EqualValues(t, 3, info.CurrentEpoch)
}
}
func TestCluster_MigrateSlot(t *testing.T) {
ctx := context.Background()
ns := "test-ns"
clusterName := "test-clusterProbe"
cluster, err := store.NewCluster(clusterName, []string{"127.0.0.1:7770", "127.0.0.1:7771"}, 1)
require.NoError(t, err)
require.NoError(t, cluster.Reset(ctx))
require.NoError(t, cluster.SyncToNodes(ctx))
defer func() {
require.NoError(t, cluster.Reset(ctx))
}()
slotRange, err := store.NewSlotRange(0, 0)
require.NoError(t, err)
require.NoError(t, cluster.MigrateSlot(ctx, slotRange, 1, false))
s := NewMockClusterStore()
require.NoError(t, s.CreateCluster(ctx, ns, cluster))
clusterProbe := NewClusterChecker(s, ns, clusterName)
clusterProbe.WithPingInterval(100 * time.Millisecond)
clusterProbe.Start()
defer clusterProbe.Close()
ticker := time.NewTicker(400 * time.Millisecond)
defer ticker.Stop()
<-ticker.C
}