vitess.io/vitess@v0.16.2/go/vt/vtgr/controller/group_test.go (about)

     1  /*
     2  Copyright 2021 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package controller
    18  
    19  import (
    20  	"math"
    21  	"testing"
    22  
    23  	"vitess.io/vitess/go/vt/vtgr/log"
    24  
    25  	"vitess.io/vitess/go/vt/vtgr/db"
    26  	"vitess.io/vitess/go/vt/vtgr/inst"
    27  
    28  	"github.com/stretchr/testify/assert"
    29  )
    30  
    31  func TestSQLGroupToString(t *testing.T) {
    32  	group := NewSQLGroup(2, true, "ks", "0")
    33  	v1 := db.NewGroupView("v1", "host1", 10)
    34  	v1.GroupName = "group_name"
    35  	var l1 []*db.GroupMember
    36  	var l2 []*db.GroupMember
    37  	m1 := db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false)
    38  	m2 := db.NewGroupMember("ONLINE", "SECONDARY", "host2", 10, true)
    39  	m3 := db.NewGroupMember("OFFLINE", "SECONDARY", "host3", 10, true)
    40  	l1 = append(l1, m1)
    41  	l1 = append(l1, m2)
    42  	v1.UnresolvedMembers = l1
    43  	l2 = append(l2, m3)
    44  	v2 := db.NewGroupView("v2", "host2", 10)
    45  	v2.GroupName = "group_name"
    46  	v2.UnresolvedMembers = l2
    47  	group.recordView(v2)
    48  	group.recordView(v1)
    49  	assert.Equal(t, `[v2] SQLGroup group=group_name | host3 SECONDARY OFFLINE readonly=true
    50  [v1] SQLGroup group=group_name | host1 PRIMARY ONLINE readonly=false | host2 SECONDARY ONLINE readonly=true
    51  `, group.ToString())
    52  	group.Resolve()
    53  	assert.Equal(t, `[v2] SQLGroup group=group_name | host3 SECONDARY OFFLINE readonly=true
    54  [v1] SQLGroup group=group_name | host1 PRIMARY ONLINE readonly=false | host2 SECONDARY ONLINE readonly=true
    55  [resolved_view]
    56  group_name=group_name
    57  [host1] state=ONLINE role=PRIMARY readonly=false
    58  [host2] state=ONLINE role=SECONDARY readonly=true
    59  [host3] state=OFFLINE role=SECONDARY readonly=true
    60  `, group.ToString())
    61  }
    62  
    63  func TestGetGroupName(t *testing.T) {
    64  	group := NewSQLGroup(3, true, "ks", "0")
    65  	v1 := db.NewGroupView("v1", "host1", 10)
    66  	v1.GroupName = "group"
    67  	v1.UnresolvedMembers = []*db.GroupMember{
    68  		db.NewGroupMember("OFFLINE", "", "host1", 10, true),
    69  	}
    70  	group.recordView(v1)
    71  	v2 := db.NewGroupView("v2", "host2", 10)
    72  	v2.GroupName = "group"
    73  	v2.UnresolvedMembers = []*db.GroupMember{
    74  		db.NewGroupMember("OFFLINE", "", "", 0, true),
    75  	}
    76  	group.recordView(v2)
    77  	err := group.Resolve()
    78  	assert.NoError(t, err)
    79  	name := group.GetGroupName()
    80  	assert.Equal(t, "group", name)
    81  	v3 := db.NewGroupView("v3", "host3", 10)
    82  	v3.GroupName = "group_foo"
    83  	group.recordView(v3)
    84  	err = group.Resolve()
    85  	assert.Errorf(t, err, "group has split brain")
    86  	name = group.GetGroupName()
    87  	// group keeps the group name before finding a divergent group name
    88  	assert.Equal(t, "group", name)
    89  }
    90  
    91  func TestIsActiveWithMultiplePrimary(t *testing.T) {
    92  	group := NewSQLGroup(2, true, "ks", "0")
    93  	v1 := db.NewGroupView("v1", "host1", 10)
    94  	v1.GroupName = "group"
    95  	v1.UnresolvedMembers = []*db.GroupMember{
    96  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
    97  		db.NewGroupMember("ONLINE", "SECONDARY", "host2", 10, true),
    98  	}
    99  	group.recordView(v1)
   100  	v2 := db.NewGroupView("v2", "host2", 10)
   101  	v2.GroupName = "group"
   102  	v2.UnresolvedMembers = []*db.GroupMember{
   103  		db.NewGroupMember("ONLINE", "SECONDARY", "host1", 10, true),
   104  		db.NewGroupMember("ONLINE", "PRIMARY", "host2", 10, false),
   105  	}
   106  	group.recordView(v2)
   107  	err := group.Resolve()
   108  	assert.Errorf(t, err, "group network partition")
   109  }
   110  
   111  func TestIsSafeToBootstrap(t *testing.T) {
   112  	group := NewSQLGroup(1, true, "ks", "0")
   113  	isSafe := group.IsSafeToBootstrap()
   114  	assert.False(t, isSafe)
   115  	v1 := db.NewGroupView("v1", "host1", 10)
   116  	v1.GroupName = "group"
   117  	v1.UnresolvedMembers = []*db.GroupMember{
   118  		db.NewGroupMember("OFFLINE", "", "", 0, true),
   119  		db.NewGroupMember("OFFLINE", "", "", 0, true),
   120  	}
   121  	group.recordView(v1)
   122  	group.Resolve()
   123  	isSafe = group.IsSafeToBootstrap()
   124  	assert.True(t, isSafe)
   125  }
   126  
   127  func TestIsSafeToBootstrapWithPrimary(t *testing.T) {
   128  	group := NewSQLGroup(1, true, "ks", "0")
   129  	v1 := db.NewGroupView("v1", "host1", 10)
   130  	v1.GroupName = "group"
   131  	// it is not safe to bootstrap if we see a primary node in group
   132  	v1.UnresolvedMembers = []*db.GroupMember{
   133  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 0, false),
   134  		db.NewGroupMember("OFFLINE", "", "", 0, true),
   135  	}
   136  	group.recordView(v1)
   137  	group.Resolve()
   138  	isSafe := group.IsSafeToBootstrap()
   139  	assert.False(t, isSafe)
   140  }
   141  
   142  func TestIsUnconnectedReplica(t *testing.T) {
   143  	group := NewSQLGroup(1, true, "ks", "0")
   144  	isSafe := group.IsSafeToBootstrap()
   145  	assert.False(t, isSafe)
   146  	v1 := db.NewGroupView("v1", "host1", 10)
   147  	v1.GroupName = "group"
   148  	v1.UnresolvedMembers = []*db.GroupMember{
   149  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
   150  		db.NewGroupMember("ONLINE", "SECONDARY", "host2", 10, true),
   151  	}
   152  	group.recordView(v1)
   153  	group.Resolve()
   154  	isUnconnected := group.IsUnconnectedReplica(&inst.InstanceKey{Hostname: "host2", Port: 10})
   155  	assert.False(t, isUnconnected)
   156  }
   157  
   158  func TestGetOnlineGroupSizeFromPrimary(t *testing.T) {
   159  	group := NewSQLGroup(1, true, "ks", "0")
   160  	isSafe := group.IsSafeToBootstrap()
   161  	assert.False(t, isSafe)
   162  	v1 := db.NewGroupView("v1", "host1", 10)
   163  	v1.GroupName = "group"
   164  	v1.UnresolvedMembers = []*db.GroupMember{
   165  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
   166  		db.NewGroupMember("ONLINE", "SECONDARY", "host2", 10, true),
   167  		db.NewGroupMember("RECOVERING", "SECONDARY", "host3", 10, true),
   168  	}
   169  	v2 := db.NewGroupView("v2", "host2", 10)
   170  	v2.GroupName = "group"
   171  	v2.UnresolvedMembers = []*db.GroupMember{}
   172  	group.recordView(v1)
   173  	group.recordView(v2)
   174  	group.Resolve()
   175  	size, readOnly := group.GetOnlineGroupInfo()
   176  	assert.Equal(t, 2, size)
   177  	assert.False(t, readOnly)
   178  }
   179  
   180  func TestNetworkPartition(t *testing.T) {
   181  	group := NewSQLGroup(3, true, "ks", "0")
   182  	v1 := db.NewGroupView("v1", "host1", 10)
   183  	v1.GroupName = "group"
   184  	v1.UnresolvedMembers = []*db.GroupMember{
   185  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
   186  		db.NewGroupMember("UNREACHABLE", "SECONDARY", "host2", 10, true),
   187  		db.NewGroupMember("UNREACHABLE", "SECONDARY", "host3", 10, true),
   188  	}
   189  	v2 := db.NewGroupView("v2", "host2", 10)
   190  	v2.GroupName = "group"
   191  	v2.UnresolvedMembers = []*db.GroupMember{
   192  		db.NewGroupMember("OFFLINE", "", "host2", 10, true),
   193  	}
   194  	v3 := db.NewGroupView("v3", "host3", 10)
   195  	v3.GroupName = "group"
   196  	v3.UnresolvedMembers = []*db.GroupMember{
   197  		db.NewGroupMember("OFFLINE", "", "host3", 10, true),
   198  	}
   199  	group.recordView(v1)
   200  	group.recordView(v2)
   201  	group.recordView(v3)
   202  	err := group.Resolve()
   203  	assert.EqualErrorf(t, err, "group backoff error", err.Error())
   204  	rv := group.resolvedView
   205  	assert.Equal(t, "group", rv.groupName)
   206  	assert.Equal(t, map[inst.InstanceKey]db.GroupMember{
   207  		{Hostname: "host1", Port: 10}: {HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   208  		{Hostname: "host2", Port: 10}: {HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.UNREACHABLE, ReadOnly: true},
   209  		{Hostname: "host3", Port: 10}: {HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.UNREACHABLE, ReadOnly: true},
   210  	}, rv.view)
   211  }
   212  
   213  func TestInconsistentState(t *testing.T) {
   214  	group := NewSQLGroup(3, true, "ks", "0")
   215  	v1 := db.NewGroupView("v1", "host1", 10)
   216  	v1.GroupName = "group"
   217  	v1.HeartbeatStaleness = 11
   218  	v1.UnresolvedMembers = []*db.GroupMember{
   219  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
   220  		db.NewGroupMember("ONLINE", "SECONDARY", "host2", 10, true),
   221  		db.NewGroupMember("ONLINE", "SECONDARY", "host3", 10, true),
   222  	}
   223  	v2 := db.NewGroupView("v2", "host2", 10)
   224  	v2.GroupName = "group"
   225  	v2.HeartbeatStaleness = 11
   226  	v2.UnresolvedMembers = []*db.GroupMember{
   227  		db.NewGroupMember("OFFLINE", "", "host2", 10, true),
   228  	}
   229  	v3 := db.NewGroupView("v3", "host3", 10)
   230  	v3.GroupName = "group"
   231  	v3.HeartbeatStaleness = 13
   232  	v3.UnresolvedMembers = []*db.GroupMember{
   233  		db.NewGroupMember("OFFLINE", "", "host3", 10, true),
   234  	}
   235  	group.recordView(v1)
   236  	group.recordView(v2)
   237  	group.recordView(v3)
   238  	group.heartbeatThreshold = 10
   239  	err := group.Resolve()
   240  	assert.EqualErrorf(t, err, "group backoff error", err.Error())
   241  	rv := group.resolvedView
   242  	assert.Equal(t, "group", rv.groupName)
   243  	assert.Nil(t, rv.view)
   244  }
   245  
   246  func TestInconsistentStateWithInvalidStaleResult(t *testing.T) {
   247  	group := NewSQLGroup(3, true, "ks", "0")
   248  	v1 := db.NewGroupView("v1", "host1", 10)
   249  	v1.GroupName = "group"
   250  	v1.HeartbeatStaleness = math.MaxInt32
   251  	v1.UnresolvedMembers = []*db.GroupMember{
   252  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
   253  		db.NewGroupMember("ONLINE", "SECONDARY", "host2", 10, true),
   254  		db.NewGroupMember("ONLINE", "SECONDARY", "host3", 10, true),
   255  	}
   256  	v2 := db.NewGroupView("v2", "host2", 10)
   257  	v2.GroupName = "group"
   258  	v2.HeartbeatStaleness = math.MaxInt32
   259  	v2.UnresolvedMembers = []*db.GroupMember{
   260  		db.NewGroupMember("OFFLINE", "", "host2", 10, true),
   261  	}
   262  	v3 := db.NewGroupView("v3", "host3", 10)
   263  	v3.GroupName = "group"
   264  	v3.HeartbeatStaleness = math.MaxInt32
   265  	v3.UnresolvedMembers = []*db.GroupMember{
   266  		db.NewGroupMember("OFFLINE", "", "host3", 10, true),
   267  	}
   268  	group.recordView(v1)
   269  	group.recordView(v2)
   270  	group.recordView(v3)
   271  	group.heartbeatThreshold = 10
   272  	err := group.Resolve()
   273  	// Same setup as TestInconsistentState but because HeartbeatStaleness are all MaxInt32
   274  	// the backoff is not triggered
   275  	assert.NoError(t, err)
   276  	rv := group.resolvedView
   277  	assert.Equal(t, "group", rv.groupName)
   278  }
   279  
   280  func TestInconsistentUnknownState(t *testing.T) {
   281  	group := NewSQLGroup(3, true, "ks", "0")
   282  	v1 := db.NewGroupView("v1", "host1", 10)
   283  	v1.GroupName = "group"
   284  	v1.UnresolvedMembers = []*db.GroupMember{
   285  		db.NewGroupMember("ONLINE", "PRIMARY", "host1", 10, false),
   286  		db.NewGroupMember("RECOVERING", "SECONDARY", "host2", 10, true),
   287  		db.NewGroupMember("ONLINE", "SECONDARY", "host3", 10, true),
   288  	}
   289  	v2 := db.NewGroupView("v2", "host2", 10)
   290  	v2.GroupName = "group"
   291  	v2.UnresolvedMembers = []*db.GroupMember{
   292  		db.NewGroupMember("", "", "host2", 10, true),
   293  	}
   294  	v3 := db.NewGroupView("v3", "host3", 10)
   295  	v3.GroupName = "group"
   296  	v3.UnresolvedMembers = []*db.GroupMember{
   297  		db.NewGroupMember("ONLINE", "SECONDARY", "host3", 10, true),
   298  	}
   299  	group.recordView(v1)
   300  	group.recordView(v2)
   301  	group.recordView(v3)
   302  	err := group.Resolve()
   303  	// host 2 reports itself with empty state
   304  	// therefore we shouldn't raise error even with inconsistent state
   305  	assert.NoError(t, err)
   306  	rv := group.resolvedView
   307  	assert.Equal(t, "group", rv.groupName)
   308  	assert.Equal(t, map[inst.InstanceKey]db.GroupMember{
   309  		{Hostname: "host1", Port: 10}: {HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   310  		{Hostname: "host2", Port: 10}: {HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.RECOVERING, ReadOnly: true},
   311  		{Hostname: "host3", Port: 10}: {HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   312  	}, rv.view)
   313  }
   314  
   315  func TestIsBootstrapInProcess(t *testing.T) {
   316  	group := NewSQLGroup(3, true, "ks", "0")
   317  	v1 := db.NewGroupView("v1", "host1", 10)
   318  	v1.GroupName = "group"
   319  	v1.UnresolvedMembers = []*db.GroupMember{
   320  		db.NewGroupMember("RECOVERING", "SECONDARY", "host1", 10, false),
   321  	}
   322  	v2 := db.NewGroupView("v2", "host2", 10)
   323  	v2.GroupName = "group"
   324  	v2.UnresolvedMembers = []*db.GroupMember{
   325  		db.NewGroupMember("OFFLINE", "", "host2", 10, false),
   326  	}
   327  	v3 := db.NewGroupView("v3", "host", 10)
   328  	v3.GroupName = "group"
   329  	v3.UnresolvedMembers = []*db.GroupMember{}
   330  	group.recordView(v1)
   331  	group.recordView(v2)
   332  	group.recordView(v3)
   333  	err := group.Resolve()
   334  	assert.Errorf(t, err, "group transient error")
   335  }
   336  
   337  func TestResolve(t *testing.T) {
   338  	healthyView := []*db.GroupMember{
   339  		{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   340  		{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   341  		{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   342  	}
   343  	var testCases = []struct {
   344  		testName string
   345  		views    []*db.GroupView
   346  		expected *ResolvedView
   347  		errorMsg string
   348  	}{
   349  		{"test healthy shard", []*db.GroupView{
   350  			{MySQLHost: "host1", MySQLPort: 10, GroupName: "group", UnresolvedMembers: healthyView},
   351  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "group", UnresolvedMembers: healthyView},
   352  			{MySQLHost: "host3", MySQLPort: 10, GroupName: "group", UnresolvedMembers: healthyView},
   353  		}, &ResolvedView{"group", map[inst.InstanceKey]db.GroupMember{
   354  			{Hostname: "host1", Port: 10}: {HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   355  			{Hostname: "host2", Port: 10}: {HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   356  			{Hostname: "host3", Port: 10}: {HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   357  		}, nil}, ""},
   358  		{"test readonly with unreachable primary", []*db.GroupView{ // host1 is unreachable
   359  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   360  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   361  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   362  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: false},
   363  			}},
   364  			{MySQLHost: "host3", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   365  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   366  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: false},
   367  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   368  			}},
   369  		}, &ResolvedView{"group", map[inst.InstanceKey]db.GroupMember{
   370  			{Hostname: "host1", Port: 10}: {HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   371  			{Hostname: "host2", Port: 10}: {HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   372  			{Hostname: "host3", Port: 10}: {HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   373  		}, nil}, ""},
   374  		{"test split brain by group name", []*db.GroupView{
   375  			{MySQLHost: "host1", MySQLPort: 10, GroupName: "group", UnresolvedMembers: healthyView},
   376  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "group1", UnresolvedMembers: healthyView},
   377  			{MySQLHost: "host3", MySQLPort: 10, GroupName: "group", UnresolvedMembers: healthyView},
   378  		}, nil, "group has split brain"},
   379  		{"test empty hostname", []*db.GroupView{
   380  			{MySQLHost: "host1", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   381  				{HostName: "", Port: 0, Role: db.UNKNOWNROLE, State: db.OFFLINE, ReadOnly: true},
   382  			}},
   383  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "", UnresolvedMembers: []*db.GroupMember{
   384  				{HostName: "host2", Port: 10, Role: db.UNKNOWNROLE, State: db.OFFLINE, ReadOnly: true},
   385  			}},
   386  			{MySQLHost: "host3", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   387  				{HostName: "host3", Port: 10, Role: db.UNKNOWNROLE, State: db.OFFLINE, ReadOnly: true},
   388  			}},
   389  		}, &ResolvedView{"group", map[inst.InstanceKey]db.GroupMember{
   390  			{Hostname: "host1", Port: 10}: {HostName: "host1", Port: 10, Role: db.UNKNOWNROLE, State: db.OFFLINE, ReadOnly: true},
   391  			{Hostname: "host2", Port: 10}: {HostName: "host2", Port: 10, Role: db.UNKNOWNROLE, State: db.OFFLINE, ReadOnly: true},
   392  			{Hostname: "host3", Port: 10}: {HostName: "host3", Port: 10, Role: db.UNKNOWNROLE, State: db.OFFLINE, ReadOnly: true},
   393  		}, nil}, ""},
   394  		{"test network partition by majority unreachable", []*db.GroupView{
   395  			{MySQLHost: "host1", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   396  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.UNREACHABLE, ReadOnly: false},
   397  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   398  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.UNREACHABLE, ReadOnly: true},
   399  			}},
   400  		}, nil, "group backoff error"},
   401  		{"test no network partition with less then majority unreachable", []*db.GroupView{
   402  			{MySQLHost: "host1", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   403  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   404  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   405  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.UNREACHABLE, ReadOnly: false},
   406  			}},
   407  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   408  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   409  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   410  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.UNREACHABLE, ReadOnly: false},
   411  			}},
   412  		}, &ResolvedView{"group", map[inst.InstanceKey]db.GroupMember{
   413  			{Hostname: "host1", Port: 10}: {HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.ONLINE, ReadOnly: false},
   414  			{Hostname: "host2", Port: 10}: {HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE, ReadOnly: true},
   415  			{Hostname: "host3", Port: 10}: {HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.UNREACHABLE, ReadOnly: false},
   416  		}, nil}, "group backoff error"},
   417  		{"test network partition by unreachable primary", []*db.GroupView{
   418  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   419  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.UNREACHABLE},
   420  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE},
   421  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE},
   422  			}},
   423  			{MySQLHost: "host3", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   424  				{HostName: "host1", Port: 10, Role: db.PRIMARY, State: db.UNREACHABLE},
   425  				{HostName: "host2", Port: 10, Role: db.SECONDARY, State: db.ONLINE},
   426  				{HostName: "host3", Port: 10, Role: db.SECONDARY, State: db.ONLINE},
   427  			}},
   428  		}, nil, "group backoff error"},
   429  		{"test bootstrap ongoing", []*db.GroupView{
   430  			{MySQLHost: "host1", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{
   431  				{HostName: "", Port: 0, Role: db.SECONDARY, State: db.RECOVERING, ReadOnly: true},
   432  			}},
   433  			{MySQLHost: "host2", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{}},
   434  			{MySQLHost: "host3", MySQLPort: 10, GroupName: "group", UnresolvedMembers: []*db.GroupMember{}},
   435  		}, nil, "group ongoing bootstrap"},
   436  	}
   437  	for _, testCase := range testCases {
   438  		t.Run(testCase.testName, func(t *testing.T) {
   439  			group := SQLGroup{views: testCase.views, statsTags: []string{"ks", "0"}, logger: log.NewVTGRLogger("ks", "0")}
   440  			err := group.Resolve()
   441  			if testCase.errorMsg != "" {
   442  				assert.EqualError(t, err, testCase.errorMsg)
   443  			} else {
   444  				assert.NoError(t, err)
   445  			}
   446  			if testCase.expected != nil {
   447  				rv := group.resolvedView
   448  				expected := testCase.expected
   449  				assert.Equal(t, expected.view, rv.view)
   450  				assert.Equal(t, expected.groupName, rv.groupName)
   451  			}
   452  		})
   453  	}
   454  }