diff --git a/pkg/keyspace/tso_keyspace_group.go b/pkg/keyspace/tso_keyspace_group.go index 73b1d812a73..5d3c9e8d431 100644 --- a/pkg/keyspace/tso_keyspace_group.go +++ b/pkg/keyspace/tso_keyspace_group.go @@ -116,7 +116,7 @@ func (m *GroupManager) GetKeyspaceGroups(startID uint32, limit int) ([]*endpoint return m.store.LoadKeyspaceGroups(startID, limit) } -// GetKeyspaceGroupByID returns the keyspace group by id. +// GetKeyspaceGroupByID returns the keyspace group by ID. func (m *GroupManager) GetKeyspaceGroupByID(id uint32) (*endpoint.KeyspaceGroup, error) { var ( kg *endpoint.KeyspaceGroup @@ -135,7 +135,7 @@ func (m *GroupManager) GetKeyspaceGroupByID(id uint32) (*endpoint.KeyspaceGroup, return kg, nil } -// DeleteKeyspaceGroupByID deletes the keyspace group by id. +// DeleteKeyspaceGroupByID deletes the keyspace group by ID. func (m *GroupManager) DeleteKeyspaceGroupByID(id uint32) (*endpoint.KeyspaceGroup, error) { var ( kg *endpoint.KeyspaceGroup @@ -165,15 +165,11 @@ func (m *GroupManager) DeleteKeyspaceGroupByID(id uint32) (*endpoint.KeyspaceGro return kg, nil } +// saveKeyspaceGroups will try to save the given keyspace groups into the storage. +// If any keyspace group already exists and `overwrite` is false, it will return ErrKeyspaceGroupExists. func (m *GroupManager) saveKeyspaceGroups(keyspaceGroups []*endpoint.KeyspaceGroup, overwrite bool) error { return m.store.RunInTxn(m.ctx, func(txn kv.Txn) error { for _, keyspaceGroup := range keyspaceGroups { - // TODO: add replica count - newKG := &endpoint.KeyspaceGroup{ - ID: keyspaceGroup.ID, - UserKind: keyspaceGroup.UserKind, - Keyspaces: keyspaceGroup.Keyspaces, - } // Check if keyspace group has already existed. oldKG, err := m.store.LoadKeyspaceGroup(txn, keyspaceGroup.ID) if err != nil { @@ -182,13 +178,19 @@ func (m *GroupManager) saveKeyspaceGroups(keyspaceGroups []*endpoint.KeyspaceGro if oldKG != nil && !overwrite { return ErrKeyspaceGroupExists } - m.store.SaveKeyspaceGroup(txn, newKG) + m.store.SaveKeyspaceGroup(txn, &endpoint.KeyspaceGroup{ + ID: keyspaceGroup.ID, + UserKind: keyspaceGroup.UserKind, + Members: keyspaceGroup.Members, + Keyspaces: keyspaceGroup.Keyspaces, + InSplit: keyspaceGroup.InSplit, + }) } return nil }) } -// GetAvailableKeyspaceGroupIDByKind returns the available keyspace group id by user kind. +// GetAvailableKeyspaceGroupIDByKind returns the available keyspace group ID by user kind. func (m *GroupManager) GetAvailableKeyspaceGroupIDByKind(userKind endpoint.UserKind) (string, error) { m.RLock() defer m.RUnlock() @@ -278,3 +280,86 @@ func (m *GroupManager) UpdateKeyspaceGroup(oldGroupID, newGroupID string, oldUse return nil } + +// SplitKeyspaceGroupByID splits the keyspace group by ID into a new keyspace group with the given new ID. +// And the keyspaces in the old keyspace group will be moved to the new keyspace group. +func (m *GroupManager) SplitKeyspaceGroupByID(id, newID uint32, keyspaces []uint32) error { + // TODO: avoid to split when the keyspaces is empty. + return m.store.RunInTxn(m.ctx, func(txn kv.Txn) error { + // Load the old keyspace group first. + oldKg, err := m.store.LoadKeyspaceGroup(txn, id) + if err != nil { + return err + } + if oldKg == nil { + return ErrKeyspaceGroupNotFound + } + // Check if the new keyspace group already exists. + newKg, err := m.store.LoadKeyspaceGroup(txn, newID) + if err != nil { + return err + } + if newKg != nil { + return ErrKeyspaceGroupExists + } + // Check if the keyspaces are all in the old keyspace group. + if len(keyspaces) > len(oldKg.Keyspaces) { + return ErrKeyspaceNotInKeyspaceGroup + } + var ( + oldKeyspaceMap = make(map[uint32]struct{}, len(oldKg.Keyspaces)) + newKeyspaceMap = make(map[uint32]struct{}, len(keyspaces)) + ) + for _, keyspace := range oldKg.Keyspaces { + oldKeyspaceMap[keyspace] = struct{}{} + } + for _, keyspace := range keyspaces { + if _, ok := oldKeyspaceMap[keyspace]; !ok { + return ErrKeyspaceNotInKeyspaceGroup + } + newKeyspaceMap[keyspace] = struct{}{} + } + // Get the split keyspace group for the old keyspace group. + splitKeyspaces := make([]uint32, 0, len(oldKg.Keyspaces)-len(keyspaces)) + for _, keyspace := range oldKg.Keyspaces { + if _, ok := newKeyspaceMap[keyspace]; !ok { + splitKeyspaces = append(splitKeyspaces, keyspace) + } + } + // Update the old keyspace group. + oldKg.Keyspaces = splitKeyspaces + if err = m.store.SaveKeyspaceGroup(txn, oldKg); err != nil { + return err + } + // Create the new split keyspace group. + return m.store.SaveKeyspaceGroup(txn, &endpoint.KeyspaceGroup{ + ID: newID, + // Keep the same user kind and members as the old keyspace group. + UserKind: oldKg.UserKind, + Members: oldKg.Members, + Keyspaces: keyspaces, + // Only set the new keyspace group in split state. + InSplit: true, + }) + }) +} + +// FinishSplitKeyspaceByID finishes the split keyspace group by ID. +func (m *GroupManager) FinishSplitKeyspaceByID(id uint32) error { + return m.store.RunInTxn(m.ctx, func(txn kv.Txn) error { + // Load the keyspace group first. + kg, err := m.store.LoadKeyspaceGroup(txn, id) + if err != nil { + return err + } + if kg == nil { + return ErrKeyspaceGroupNotFound + } + // Check if it's in the split state. + if !kg.InSplit { + return ErrKeyspaceGroupNotInSplit + } + kg.InSplit = false + return m.store.SaveKeyspaceGroup(txn, kg) + }) +} diff --git a/pkg/keyspace/tso_keyspace_group_test.go b/pkg/keyspace/tso_keyspace_group_test.go index b2d835dff03..bece8209eb6 100644 --- a/pkg/keyspace/tso_keyspace_group_test.go +++ b/pkg/keyspace/tso_keyspace_group_test.go @@ -63,8 +63,9 @@ func (suite *keyspaceGroupTestSuite) TestKeyspaceGroupOperations() { UserKind: endpoint.Standard.String(), }, { - ID: uint32(2), - UserKind: endpoint.Standard.String(), + ID: uint32(2), + UserKind: endpoint.Standard.String(), + Keyspaces: []uint32{111, 222, 333}, }, { ID: uint32(3), @@ -86,10 +87,12 @@ func (suite *keyspaceGroupTestSuite) TestKeyspaceGroupOperations() { re.NoError(err) re.Equal(uint32(0), kg.ID) re.Equal(endpoint.Basic.String(), kg.UserKind) + re.False(kg.InSplit) kg, err = suite.kgm.GetKeyspaceGroupByID(3) re.NoError(err) re.Equal(uint32(3), kg.ID) re.Equal(endpoint.Standard.String(), kg.UserKind) + re.False(kg.InSplit) // remove the keyspace group 3 kg, err = suite.kgm.DeleteKeyspaceGroupByID(3) re.NoError(err) @@ -98,7 +101,6 @@ func (suite *keyspaceGroupTestSuite) TestKeyspaceGroupOperations() { kg, err = suite.kgm.GetKeyspaceGroupByID(3) re.NoError(err) re.Empty(kg) - // create an existing keyspace group keyspaceGroups = []*endpoint.KeyspaceGroup{{ID: uint32(1), UserKind: endpoint.Standard.String()}} err = suite.kgm.CreateKeyspaceGroups(keyspaceGroups) @@ -227,3 +229,55 @@ func (suite *keyspaceGroupTestSuite) TestUpdateKeyspace() { re.NoError(err) re.Len(kg3.Keyspaces, 1) } + +func (suite *keyspaceGroupTestSuite) TestKeyspaceGroupSplit() { + re := suite.Require() + + keyspaceGroups := []*endpoint.KeyspaceGroup{ + { + ID: uint32(1), + UserKind: endpoint.Basic.String(), + }, + { + ID: uint32(2), + UserKind: endpoint.Standard.String(), + Keyspaces: []uint32{111, 222, 333}, + }, + } + err := suite.kgm.CreateKeyspaceGroups(keyspaceGroups) + re.NoError(err) + // split the keyspace group 2 to 4 + err = suite.kgm.SplitKeyspaceGroupByID(2, 4, []uint32{333}) + re.NoError(err) + kg2, err := suite.kgm.GetKeyspaceGroupByID(2) + re.NoError(err) + re.Equal(uint32(2), kg2.ID) + re.Equal([]uint32{111, 222}, kg2.Keyspaces) + re.False(kg2.InSplit) + kg4, err := suite.kgm.GetKeyspaceGroupByID(4) + re.NoError(err) + re.Equal(uint32(4), kg4.ID) + re.Equal([]uint32{333}, kg4.Keyspaces) + re.True(kg4.InSplit) + re.Equal(kg2.UserKind, kg4.UserKind) + re.Equal(kg2.Members, kg4.Members) + // finish the split of keyspace group 4 + err = suite.kgm.FinishSplitKeyspaceByID(4) + re.NoError(err) + kg4, err = suite.kgm.GetKeyspaceGroupByID(4) + re.NoError(err) + re.Equal(uint32(4), kg4.ID) + re.False(kg4.InSplit) + // split a non-existing keyspace group + err = suite.kgm.SplitKeyspaceGroupByID(3, 5, nil) + re.ErrorIs(err, ErrKeyspaceGroupNotFound) + // finish the split of a non-existing keyspace group + err = suite.kgm.FinishSplitKeyspaceByID(5) + re.ErrorIs(err, ErrKeyspaceGroupNotFound) + // split into an existing keyspace group + err = suite.kgm.SplitKeyspaceGroupByID(2, 4, nil) + re.ErrorIs(err, ErrKeyspaceGroupExists) + // split with the wrong keyspaces. + err = suite.kgm.SplitKeyspaceGroupByID(2, 5, []uint32{111, 222, 444}) + re.ErrorIs(err, ErrKeyspaceNotInKeyspaceGroup) +} diff --git a/pkg/keyspace/util.go b/pkg/keyspace/util.go index 4e0bee8026b..0c12c696797 100644 --- a/pkg/keyspace/util.go +++ b/pkg/keyspace/util.go @@ -43,8 +43,14 @@ var ( ErrKeyspaceExists = errors.New("keyspace already exists") // ErrKeyspaceGroupExists indicates target keyspace group already exists. ErrKeyspaceGroupExists = errors.New("keyspace group already exists") - errModifyDefault = errors.New("cannot modify default keyspace's state") - errIllegalOperation = errors.New("unknown operation") + // ErrKeyspaceGroupNotFound is used to indicate target keyspace group does not exist. + ErrKeyspaceGroupNotFound = errors.New("keyspace group does not exist") + // ErrKeyspaceGroupNotInSplit is used to indicate target keyspace group is not in split state. + ErrKeyspaceGroupNotInSplit = errors.New("keyspace group is not in split state") + // ErrKeyspaceNotInKeyspaceGroup is used to indicate target keyspace is not in this keyspace group. + ErrKeyspaceNotInKeyspaceGroup = errors.New("keyspace is not in this keyspace group") + errModifyDefault = errors.New("cannot modify default keyspace's state") + errIllegalOperation = errors.New("unknown operation") // stateTransitionTable lists all allowed next state for the given current state. // Note that transit from any state to itself is allowed for idempotence. diff --git a/pkg/storage/endpoint/tso_keyspace_group.go b/pkg/storage/endpoint/tso_keyspace_group.go index aca0b847dac..a515018fd12 100644 --- a/pkg/storage/endpoint/tso_keyspace_group.go +++ b/pkg/storage/endpoint/tso_keyspace_group.go @@ -69,6 +69,8 @@ type KeyspaceGroupMember struct { type KeyspaceGroup struct { ID uint32 `json:"id"` UserKind string `json:"user-kind"` + // InSplit indicates whether the keyspace group is in split. + InSplit bool `json:"in-split"` // Members are the election members which campaign for the primary of the keyspace group. Members []KeyspaceGroupMember `json:"members"` // Keyspaces are the keyspace IDs which belong to the keyspace group. @@ -90,7 +92,7 @@ type KeyspaceGroupStorage interface { var _ KeyspaceGroupStorage = (*StorageEndpoint)(nil) -// LoadKeyspaceGroup loads the keyspace group by id. +// LoadKeyspaceGroup loads the keyspace group by ID. func (se *StorageEndpoint) LoadKeyspaceGroup(txn kv.Txn, id uint32) (*KeyspaceGroup, error) { value, err := txn.Load(KeyspaceGroupIDPath(id)) if err != nil || value == "" { diff --git a/pkg/tso/keyspace_group_manager.go b/pkg/tso/keyspace_group_manager.go index 95157f74b3a..12b4c40505d 100644 --- a/pkg/tso/keyspace_group_manager.go +++ b/pkg/tso/keyspace_group_manager.go @@ -410,9 +410,8 @@ func (kgm *KeyspaceGroupManager) watchKeyspaceGroupsMetaChange(revision int64) ( log.Warn("failed to unmarshal keyspace group", zap.Uint32("keysapce-group-id", id), zap.Error(errs.ErrJSONUnmarshal.Wrap(err).FastGenWithCause())) - } else { - kgm.updateKeyspaceGroup(group) } + kgm.updateKeyspaceGroup(group) case clientv3.EventTypeDelete: kgm.deleteKeyspaceGroup(id) } @@ -466,6 +465,7 @@ func (kgm *KeyspaceGroupManager) updateKeyspaceGroup(group *endpoint.KeyspaceGro zap.String("participant-name", uniqueName), zap.Uint64("participant-id", uniqueID)) + // TODO: handle the keyspace group & TSO split logic. participant := member.NewParticipant(kgm.etcdClient) participant.InitInfo( uniqueName, uniqueID, path.Join(kgm.tsoSvcRootPath, fmt.Sprintf("%05d", group.ID)), diff --git a/server/apiv2/handlers/tso_keyspace_group.go b/server/apiv2/handlers/tso_keyspace_group.go index a147784d967..fed09336c46 100644 --- a/server/apiv2/handlers/tso_keyspace_group.go +++ b/server/apiv2/handlers/tso_keyspace_group.go @@ -35,6 +35,8 @@ func RegisterTSOKeyspaceGroup(r *gin.RouterGroup) { router.GET("", GetKeyspaceGroups) router.GET("/:id", GetKeyspaceGroupByID) router.DELETE("/:id", DeleteKeyspaceGroupByID) + router.POST("/:id/split", SplitKeyspaceGroupByID) + router.DELETE("/:id/split", FinishSplitKeyspaceByID) } // CreateKeyspaceGroupParams defines the params for creating keyspace groups. @@ -44,15 +46,12 @@ type CreateKeyspaceGroupParams struct { // CreateKeyspaceGroups creates keyspace groups. func CreateKeyspaceGroups(c *gin.Context) { - svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) - manager := svr.GetKeyspaceGroupManager() createParams := &CreateKeyspaceGroupParams{} err := c.BindJSON(createParams) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, errs.ErrBindJSON.Wrap(err).GenWithStackByCause()) return } - for _, keyspaceGroup := range createParams.KeyspaceGroups { if !isValid(keyspaceGroup.ID) { c.AbortWithStatusJSON(http.StatusBadRequest, "invalid keyspace group id") @@ -60,6 +59,8 @@ func CreateKeyspaceGroups(c *gin.Context) { } } + svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) + manager := svr.GetKeyspaceGroupManager() err = manager.CreateKeyspaceGroups(createParams.KeyspaceGroups) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) @@ -71,13 +72,14 @@ func CreateKeyspaceGroups(c *gin.Context) { // GetKeyspaceGroups gets keyspace groups from the start ID with limit. // If limit is 0, it will load all keyspace groups from the start ID. func GetKeyspaceGroups(c *gin.Context) { - svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) - manager := svr.GetKeyspaceGroupManager() scanStart, scanLimit, err := parseLoadAllQuery(c) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, err.Error()) return } + + svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) + manager := svr.GetKeyspaceGroupManager() keyspaceGroups, err := manager.GetKeyspaceGroups(scanStart, scanLimit) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) @@ -87,13 +89,14 @@ func GetKeyspaceGroups(c *gin.Context) { c.IndentedJSON(http.StatusOK, keyspaceGroups) } -// GetKeyspaceGroupByID gets keyspace group by id. +// GetKeyspaceGroupByID gets keyspace group by ID. func GetKeyspaceGroupByID(c *gin.Context) { id, err := validateKeyspaceGroupID(c) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, "invalid keyspace group id") return } + svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) manager := svr.GetKeyspaceGroupManager() kg, err := manager.GetKeyspaceGroupByID(id) @@ -105,13 +108,14 @@ func GetKeyspaceGroupByID(c *gin.Context) { c.IndentedJSON(http.StatusOK, kg) } -// DeleteKeyspaceGroupByID deletes keyspace group by id. +// DeleteKeyspaceGroupByID deletes keyspace group by ID. func DeleteKeyspaceGroupByID(c *gin.Context) { id, err := validateKeyspaceGroupID(c) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, "invalid keyspace group id") return } + svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) manager := svr.GetKeyspaceGroupManager() kg, err := manager.DeleteKeyspaceGroupByID(id) @@ -122,6 +126,63 @@ func DeleteKeyspaceGroupByID(c *gin.Context) { c.IndentedJSON(http.StatusOK, kg) } +// SplitKeyspaceGroupByIDParams defines the params for splitting a keyspace group. +type SplitKeyspaceGroupByIDParams struct { + NewID uint32 `json:"new-id"` + Keyspaces []uint32 `json:"keyspaces"` +} + +// SplitKeyspaceGroupByID splits keyspace group by ID into a new keyspace group with the given new ID. +// And the keyspaces in the old keyspace group will be moved to the new keyspace group. +func SplitKeyspaceGroupByID(c *gin.Context) { + id, err := validateKeyspaceGroupID(c) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, "invalid keyspace group id") + return + } + splitParams := &SplitKeyspaceGroupByIDParams{} + err = c.BindJSON(splitParams) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, errs.ErrBindJSON.Wrap(err).GenWithStackByCause()) + return + } + if !isValid(splitParams.NewID) { + c.AbortWithStatusJSON(http.StatusBadRequest, "invalid keyspace group id") + return + } + if len(splitParams.Keyspaces) == 0 { + c.AbortWithStatusJSON(http.StatusBadRequest, "invalid empty keyspaces") + return + } + + svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) + manager := svr.GetKeyspaceGroupManager() + err = manager.SplitKeyspaceGroupByID(id, splitParams.NewID, splitParams.Keyspaces) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) + return + } + c.JSON(http.StatusOK, nil) +} + +// FinishSplitKeyspaceByID finishes split keyspace group by ID. +func FinishSplitKeyspaceByID(c *gin.Context) { + id, err := validateKeyspaceGroupID(c) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, "invalid keyspace group id") + return + } + + svr := c.MustGet(middlewares.ServerContextKey).(*server.Server) + manager := svr.GetKeyspaceGroupManager() + err = manager.FinishSplitKeyspaceByID(id) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) + return + } + c.JSON(http.StatusOK, nil) +} + func validateKeyspaceGroupID(c *gin.Context) (uint32, error) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { diff --git a/tests/server/apiv2/handlers/tso_keyspace_group_test.go b/tests/server/apiv2/handlers/tso_keyspace_group_test.go index c800af324df..00147ea0f83 100644 --- a/tests/server/apiv2/handlers/tso_keyspace_group_test.go +++ b/tests/server/apiv2/handlers/tso_keyspace_group_test.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "testing" @@ -90,7 +91,45 @@ func (suite *keyspaceGroupTestSuite) TestLoadKeyspaceGroup() { mustCreateKeyspaceGroup(re, suite.server, kgs) resp := sendLoadKeyspaceGroupRequest(re, suite.server, "0", "0") - re.Equal(3, len(resp)) + re.Len(resp, 3) +} + +func (suite *keyspaceGroupTestSuite) TestSplitKeyspaceGroup() { + re := suite.Require() + kgs := &handlers.CreateKeyspaceGroupParams{KeyspaceGroups: []*endpoint.KeyspaceGroup{ + { + ID: uint32(1), + UserKind: endpoint.Standard.String(), + Keyspaces: []uint32{111, 222, 333}, + }, + }} + + mustCreateKeyspaceGroup(re, suite.server, kgs) + resp := sendLoadKeyspaceGroupRequest(re, suite.server, "0", "0") + re.Len(resp, 2) + mustSplitKeyspaceGroup(re, suite.server, 1, &handlers.SplitKeyspaceGroupByIDParams{ + NewID: uint32(2), + Keyspaces: []uint32{111, 222}, + }) + resp = sendLoadKeyspaceGroupRequest(re, suite.server, "0", "0") + re.Len(resp, 3) + // Check keyspace group 1. + kg1 := mustLoadKeyspaceGroupByID(re, suite.server, 1) + re.Equal(uint32(1), kg1.ID) + re.Equal([]uint32{333}, kg1.Keyspaces) + re.False(kg1.InSplit) + // Check keyspace group 2. + kg2 := mustLoadKeyspaceGroupByID(re, suite.server, 2) + re.Equal(uint32(2), kg2.ID) + re.Equal([]uint32{111, 222}, kg2.Keyspaces) + re.True(kg2.InSplit) + // They should have the same user kind and members. + re.Equal(kg1.UserKind, kg2.UserKind) + re.Equal(kg1.Members, kg2.Members) + // Finish the split and check the split state. + mustFinishSplitKeyspaceGroup(re, suite.server, 2) + kg2 = mustLoadKeyspaceGroupByID(re, suite.server, 2) + re.False(kg2.InSplit) } func sendLoadKeyspaceGroupRequest(re *require.Assertions, server *tests.TestServer, token, limit string) []*endpoint.KeyspaceGroup { @@ -114,6 +153,20 @@ func sendLoadKeyspaceGroupRequest(re *require.Assertions, server *tests.TestServ return resp } +func mustLoadKeyspaceGroupByID(re *require.Assertions, server *tests.TestServer, id uint32) *endpoint.KeyspaceGroup { + httpReq, err := http.NewRequest(http.MethodGet, server.GetAddr()+keyspaceGroupsPrefix+fmt.Sprintf("/%d", id), nil) + re.NoError(err) + resp, err := dialClient.Do(httpReq) + re.NoError(err) + defer resp.Body.Close() + re.Equal(http.StatusOK, resp.StatusCode) + data, err := io.ReadAll(resp.Body) + re.NoError(err) + var kg endpoint.KeyspaceGroup + re.NoError(json.Unmarshal(data, &kg)) + return &kg +} + func mustCreateKeyspaceGroup(re *require.Assertions, server *tests.TestServer, request *handlers.CreateKeyspaceGroupParams) { data, err := json.Marshal(request) re.NoError(err) @@ -124,3 +177,25 @@ func mustCreateKeyspaceGroup(re *require.Assertions, server *tests.TestServer, r defer resp.Body.Close() re.Equal(http.StatusOK, resp.StatusCode) } + +func mustSplitKeyspaceGroup(re *require.Assertions, server *tests.TestServer, id uint32, request *handlers.SplitKeyspaceGroupByIDParams) { + data, err := json.Marshal(request) + re.NoError(err) + httpReq, err := http.NewRequest(http.MethodPost, server.GetAddr()+keyspaceGroupsPrefix+fmt.Sprintf("/%d/split", id), bytes.NewBuffer(data)) + re.NoError(err) + // Send request. + resp, err := dialClient.Do(httpReq) + re.NoError(err) + defer resp.Body.Close() + re.Equal(http.StatusOK, resp.StatusCode) +} + +func mustFinishSplitKeyspaceGroup(re *require.Assertions, server *tests.TestServer, id uint32) { + httpReq, err := http.NewRequest(http.MethodDelete, server.GetAddr()+keyspaceGroupsPrefix+fmt.Sprintf("/%d/split", id), nil) + re.NoError(err) + // Send request. + resp, err := dialClient.Do(httpReq) + re.NoError(err) + defer resp.Body.Close() + re.Equal(http.StatusOK, resp.StatusCode) +}