You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kvrocks.apache.org by hu...@apache.org on 2025/05/13 04:11:01 UTC

(kvrocks-controller) branch unstable updated: Fix the migrating slot didn't compatible with old format (#310)

This is an automated email from the ASF dual-hosted git repository.

hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks-controller.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 2c834b4  Fix the migrating slot didn't compatible with old format (#310)
2c834b4 is described below

commit 2c834b4af8dab0e44b1745d4dc28618cf9328339
Author: Byron Seto <by...@stackadapt.com>
AuthorDate: Mon May 12 22:10:57 2025 -0600

    Fix the migrating slot didn't compatible with old format (#310)
---
 cmd/client/command/helper.go |   2 +-
 controller/cluster.go        |  13 +---
 controller/cluster_test.go   |   4 +-
 server/api/cluster.go        |   2 +-
 server/api/cluster_test.go   |   4 +-
 store/cluster.go             |   4 +-
 store/cluster_node.go        |   9 +--
 store/cluster_shard.go       |  20 ++++--
 store/cluster_shard_test.go  |  17 +++--
 store/cluster_test.go        |   6 +-
 store/slot.go                |  95 +++++++++++++++++++++++---
 store/slot_test.go           | 155 +++++++++++++++++++++++++++++++++++++------
 12 files changed, 260 insertions(+), 71 deletions(-)

diff --git a/cmd/client/command/helper.go b/cmd/client/command/helper.go
index 1d395ac..ade4f35 100644
--- a/cmd/client/command/helper.go
+++ b/cmd/client/command/helper.go
@@ -51,7 +51,7 @@ func printCluster(cluster *store.Cluster) {
 				role = strings.ToUpper(store.RoleMaster)
 			}
 			migratingStatus := "NO"
-			if shard.MigratingSlot != nil {
+			if shard.IsMigrating() {
 				migratingStatus = fmt.Sprintf("%s --> %d", shard.MigratingSlot, shard.TargetShardIndex)
 			}
 			columns := []string{fmt.Sprintf("%d", i), node.ID(), node.Addr(), role, migratingStatus}
diff --git a/controller/cluster.go b/controller/cluster.go
index 2d6802e..2cb08b5 100755
--- a/controller/cluster.go
+++ b/controller/cluster.go
@@ -318,19 +318,12 @@ func (c *ClusterChecker) tryUpdateMigrationStatus(ctx context.Context, clonedClu
 		if !shard.IsMigrating() {
 			continue
 		}
-
 		sourceNodeClusterInfo, err := shard.GetMasterNode().GetClusterInfo(ctx)
 		if err != nil {
 			log.Error("Failed to get the cluster info from the source node", zap.Error(err))
 			return
 		}
-		if sourceNodeClusterInfo.MigratingSlot == nil {
-			log.Error("The source migration slot is empty",
-				zap.String("migrating_slot", shard.MigratingSlot.String()),
-			)
-			return
-		}
-		if !sourceNodeClusterInfo.MigratingSlot.Equal(shard.MigratingSlot) {
+		if !sourceNodeClusterInfo.MigratingSlot.Equal(shard.MigratingSlot.SlotRange) {
 			log.Error("Mismatch migrating slot",
 				zap.String("source_migrating_slot", sourceNodeClusterInfo.MigratingSlot.String()),
 				zap.String("migrating_slot", shard.MigratingSlot.String()),
@@ -355,9 +348,9 @@ func (c *ClusterChecker) tryUpdateMigrationStatus(ctx context.Context, clonedClu
 			c.updateCluster(clonedCluster)
 			log.Warn("Failed to migrate the slot", zap.String("slot", migratingSlot.String()))
 		case "success":
-			clonedCluster.Shards[i].SlotRanges = store.RemoveSlotFromSlotRanges(clonedCluster.Shards[i].SlotRanges, *shard.MigratingSlot)
+			clonedCluster.Shards[i].SlotRanges = store.RemoveSlotFromSlotRanges(clonedCluster.Shards[i].SlotRanges, shard.MigratingSlot.SlotRange)
 			clonedCluster.Shards[shard.TargetShardIndex].SlotRanges = store.AddSlotToSlotRanges(
-				clonedCluster.Shards[shard.TargetShardIndex].SlotRanges, *shard.MigratingSlot,
+				clonedCluster.Shards[shard.TargetShardIndex].SlotRanges, shard.MigratingSlot.SlotRange,
 			)
 			migratedSlot := shard.MigratingSlot
 			clonedCluster.Shards[i].ClearMigrateState()
diff --git a/controller/cluster_test.go b/controller/cluster_test.go
index d415f3f..a1a720a 100644
--- a/controller/cluster_test.go
+++ b/controller/cluster_test.go
@@ -109,7 +109,7 @@ func TestCluster_FailureCount(t *testing.T) {
 				mockNode0, mockNode1, mockNode2, mockNode3,
 			},
 			SlotRanges:       []store.SlotRange{{Start: 0, Stop: 16383}},
-			MigratingSlot:    nil,
+			MigratingSlot:    &store.MigratingSlot{IsMigrating: false},
 			TargetShardIndex: -1,
 		}},
 	}
@@ -221,7 +221,7 @@ func TestCluster_MigrateSlot(t *testing.T) {
 	}()
 	slotRange, err := store.NewSlotRange(0, 0)
 	require.NoError(t, err)
-	require.NoError(t, cluster.MigrateSlot(ctx, *slotRange, 1, false))
+	require.NoError(t, cluster.MigrateSlot(ctx, slotRange, 1, false))
 
 	s := NewMockClusterStore()
 	require.NoError(t, s.CreateCluster(ctx, ns, cluster))
diff --git a/server/api/cluster.go b/server/api/cluster.go
index b539c90..b223644 100644
--- a/server/api/cluster.go
+++ b/server/api/cluster.go
@@ -33,7 +33,7 @@ import (
 
 type MigrateSlotRequest struct {
 	Target   int             `json:"target" validate:"required"`
-	Slot     store.SlotRange `json:"slot" validate:"required"`
+	Slot     store.SlotRange `json:"slot" validate:"required"` // we don't use store.MigratingSlot here because we expect a valid SlotRange
 	SlotOnly bool            `json:"slot_only"`
 }
 
diff --git a/server/api/cluster_test.go b/server/api/cluster_test.go
index 6559a87..5d3aa8d 100644
--- a/server/api/cluster_test.go
+++ b/server/api/cluster_test.go
@@ -131,7 +131,7 @@ func TestClusterBasics(t *testing.T) {
 		slotRange, err := store.NewSlotRange(3, 3)
 		require.NoError(t, err)
 		testMigrateReq := &MigrateSlotRequest{
-			Slot:     *slotRange,
+			Slot:     slotRange,
 			SlotOnly: true,
 			Target:   1,
 		}
@@ -271,7 +271,7 @@ func TestClusterMigrateData(t *testing.T) {
 			currentVersion := gotCluster.Version.Load()
 			sourceSlotRanges := gotCluster.Shards[0].SlotRanges
 			targetSlotRanges := gotCluster.Shards[1].SlotRanges
-			require.EqualValues(t, slotRange, *gotCluster.Shards[0].MigratingSlot)
+			require.EqualValues(t, slotRange, gotCluster.Shards[0].MigratingSlot.SlotRange)
 			require.EqualValues(t, 1, gotCluster.Shards[0].TargetShardIndex)
 
 			// Run the controller to check and update the migration status
diff --git a/store/cluster.go b/store/cluster.go
index b4bfd9e..8dff6ed 100644
--- a/store/cluster.go
+++ b/store/cluster.go
@@ -181,7 +181,7 @@ func (cluster *Cluster) findShardIndexBySlot(slot SlotRange) (int, error) {
 	for i := 0; i < len(cluster.Shards); i++ {
 		slotRanges := cluster.Shards[i].SlotRanges
 		for _, slotRange := range slotRanges {
-			if slotRange.HasOverlap(&slot) {
+			if slotRange.HasOverlap(slot) {
 				if sourceShardIdx != -1 {
 					return sourceShardIdx, consts.ErrSlotRangeBelongsToMultipleShards
 				}
@@ -226,7 +226,7 @@ func (cluster *Cluster) MigrateSlot(ctx context.Context, slot SlotRange, targetS
 	}
 
 	// Will start the data migration in the background
-	cluster.Shards[sourceShardIdx].MigratingSlot = &slot
+	cluster.Shards[sourceShardIdx].MigratingSlot = FromSlotRange(slot)
 	cluster.Shards[sourceShardIdx].TargetShardIndex = targetShardIdx
 	return nil
 }
diff --git a/store/cluster_node.go b/store/cluster_node.go
index 4da4253..8d038d0 100644
--- a/store/cluster_node.go
+++ b/store/cluster_node.go
@@ -85,9 +85,9 @@ type ClusterNode struct {
 }
 
 type ClusterInfo struct {
-	CurrentEpoch   int64      `json:"cluster_current_epoch"`
-	MigratingSlot  *SlotRange `json:"migrating_slot"`
-	MigratingState string     `json:"migrating_state"`
+	CurrentEpoch   int64          `json:"cluster_current_epoch"`
+	MigratingSlot  *MigratingSlot `json:"migrating_slot"`
+	MigratingState string         `json:"migrating_state"`
 }
 
 type ClusterNodeInfo struct {
@@ -195,10 +195,11 @@ func (n *ClusterNode) GetClusterInfo(ctx context.Context) (*ClusterInfo, error)
 			}
 		case "migrating_slot", "migrating_slot(s)":
 			// TODO(@git-hulk): handle multiple migrating slots
-			clusterInfo.MigratingSlot, err = ParseSlotRange(fields[1])
+			slotRange, err := ParseSlotRange(fields[1])
 			if err != nil {
 				return nil, err
 			}
+			clusterInfo.MigratingSlot = FromSlotRange(*slotRange)
 		case "migrating_state":
 			clusterInfo.MigratingState = fields[1]
 		}
diff --git a/store/cluster_shard.go b/store/cluster_shard.go
index 62c8826..69ffd4a 100644
--- a/store/cluster_shard.go
+++ b/store/cluster_shard.go
@@ -33,11 +33,17 @@ import (
 	"github.com/apache/kvrocks-controller/consts"
 )
 
+const (
+	// the old migrating slot was denoted by an int and -1 was
+	// used to denote a non migrating slot
+	NotMigratingInt = -1
+)
+
 type Shard struct {
-	Nodes            []Node      `json:"nodes"`
-	SlotRanges       []SlotRange `json:"slot_ranges"`
-	TargetShardIndex int         `json:"target_shard_index"`
-	MigratingSlot    *SlotRange  `json:"migrating_slot"`
+	Nodes            []Node         `json:"nodes"`
+	SlotRanges       []SlotRange    `json:"slot_ranges"`
+	TargetShardIndex int            `json:"target_shard_index"`
+	MigratingSlot    *MigratingSlot `json:"migrating_slot"`
 }
 
 type Shards []*Shard
@@ -112,7 +118,7 @@ func (shard *Shard) addNode(addr, role, password string) error {
 }
 
 func (shard *Shard) IsMigrating() bool {
-	return shard.MigratingSlot != nil && shard.TargetShardIndex != -1
+	return shard.MigratingSlot != nil && shard.MigratingSlot.IsMigrating && shard.TargetShardIndex != -1
 }
 
 func (shard *Shard) GetMasterNode() Node {
@@ -206,7 +212,7 @@ func (shard *Shard) promoteNewMaster(ctx context.Context, masterNodeID, preferre
 	return preferredNewMasterNode.ID(), nil
 }
 
-func (shard *Shard) HasOverlap(slotRange *SlotRange) bool {
+func (shard *Shard) HasOverlap(slotRange SlotRange) bool {
 	for _, shardSlotRange := range shard.SlotRanges {
 		if shardSlotRange.HasOverlap(slotRange) {
 			return true
@@ -261,7 +267,7 @@ func (shard *Shard) UnmarshalJSON(bytes []byte) error {
 	var data struct {
 		SlotRanges       []SlotRange    `json:"slot_ranges"`
 		TargetShardIndex int            `json:"target_shard_index"`
-		MigratingSlot    *SlotRange     `json:"migrating_slot"`
+		MigratingSlot    *MigratingSlot `json:"migrating_slot"`
 		Nodes            []*ClusterNode `json:"nodes"`
 	}
 	if err := json.Unmarshal(bytes, &data); err != nil {
diff --git a/store/cluster_shard_test.go b/store/cluster_shard_test.go
index 994046f..1406f35 100644
--- a/store/cluster_shard_test.go
+++ b/store/cluster_shard_test.go
@@ -29,11 +29,11 @@ import (
 
 func TestShard_HasOverlap(t *testing.T) {
 	shard := NewShard()
-	slotRange := &SlotRange{Start: 0, Stop: 100}
-	shard.SlotRanges = append(shard.SlotRanges, *slotRange)
+	slotRange := SlotRange{Start: 0, Stop: 100}
+	shard.SlotRanges = append(shard.SlotRanges, slotRange)
 	require.True(t, shard.HasOverlap(slotRange))
-	require.True(t, shard.HasOverlap(&SlotRange{Start: 50, Stop: 150}))
-	require.False(t, shard.HasOverlap(&SlotRange{Start: 101, Stop: 150}))
+	require.True(t, shard.HasOverlap(SlotRange{Start: 50, Stop: 150}))
+	require.False(t, shard.HasOverlap(SlotRange{Start: 101, Stop: 150}))
 }
 
 func TestShard_Sort(t *testing.T) {
@@ -56,17 +56,22 @@ func TestShard_Sort(t *testing.T) {
 func TestShard_IsServicing(t *testing.T) {
 	var err error
 	shard := NewShard()
+	shard.TargetShardIndex = 0
+	shard.MigratingSlot = &MigratingSlot{IsMigrating: false}
+	require.False(t, shard.IsServicing())
+
 	shard.TargetShardIndex = 0
 	shard.MigratingSlot = nil
 	require.False(t, shard.IsServicing())
 
 	shard.TargetShardIndex = 0
-	shard.MigratingSlot, err = NewSlotRange(1, 1)
+	slotRange, err := NewSlotRange(1, 1)
 	require.Nil(t, err)
+	shard.MigratingSlot = FromSlotRange(slotRange)
 	require.True(t, shard.IsServicing())
 
 	shard.TargetShardIndex = -1
-	shard.MigratingSlot = nil
+	shard.MigratingSlot = &MigratingSlot{IsMigrating: false}
 	shard.SlotRanges = []SlotRange{{Start: 0, Stop: 100}}
 	require.True(t, shard.IsServicing())
 
diff --git a/store/cluster_test.go b/store/cluster_test.go
index 6b6e45c..975f03f 100644
--- a/store/cluster_test.go
+++ b/store/cluster_test.go
@@ -44,19 +44,19 @@ func TestCluster_FindIndexShardBySlot(t *testing.T) {
 
 	slotRange, err := NewSlotRange(0, 0)
 	require.NoError(t, err)
-	shard, err := cluster.findShardIndexBySlot(*slotRange)
+	shard, err := cluster.findShardIndexBySlot(slotRange)
 	require.NoError(t, err)
 	require.Equal(t, 0, shard)
 
 	slotRange, err = NewSlotRange(MaxSlotID/3+1, MaxSlotID/3+1)
 	require.NoError(t, err)
-	shard, err = cluster.findShardIndexBySlot(*slotRange)
+	shard, err = cluster.findShardIndexBySlot(slotRange)
 	require.NoError(t, err)
 	require.Equal(t, 1, shard)
 
 	slotRange, err = NewSlotRange(MaxSlotID, MaxSlotID)
 	require.NoError(t, err)
-	shard, err = cluster.findShardIndexBySlot(*slotRange)
+	shard, err = cluster.findShardIndexBySlot(slotRange)
 	require.NoError(t, err)
 	require.Equal(t, 2, shard)
 }
diff --git a/store/slot.go b/store/slot.go
index 04a2b52..448002e 100644
--- a/store/slot.go
+++ b/store/slot.go
@@ -44,24 +44,26 @@ type SlotRange struct {
 
 type SlotRanges []SlotRange
 
-func NewSlotRange(start, stop int) (*SlotRange, error) {
+type MigratingSlot struct {
+	SlotRange
+	IsMigrating bool
+}
+
+func NewSlotRange(start, stop int) (SlotRange, error) {
 	if start > stop {
-		return nil, errors.New("start was larger than stop")
+		return SlotRange{}, errors.New("start was larger than stop")
 	}
 	if (start < MinSlotID || start > MaxSlotID) ||
 		(stop < MinSlotID || stop > MaxSlotID) {
-		return nil, ErrSlotOutOfRange
+		return SlotRange{}, ErrSlotOutOfRange
 	}
-	return &SlotRange{
+	return SlotRange{
 		Start: start,
 		Stop:  stop,
 	}, nil
 }
 
-func (slotRange *SlotRange) Equal(that *SlotRange) bool {
-	if that == nil {
-		return false
-	}
+func (slotRange *SlotRange) Equal(that SlotRange) bool {
 	if slotRange.Start != that.Start {
 		return false
 	}
@@ -71,7 +73,7 @@ func (slotRange *SlotRange) Equal(that *SlotRange) bool {
 	return true
 }
 
-func (slotRange *SlotRange) HasOverlap(that *SlotRange) bool {
+func (slotRange *SlotRange) HasOverlap(that SlotRange) bool {
 	return slotRange.Stop >= that.Start && slotRange.Start <= that.Stop
 }
 
@@ -156,13 +158,84 @@ func (SlotRanges *SlotRanges) Contains(slot int) bool {
 
 func (SlotRanges *SlotRanges) HasOverlap(slotRange SlotRange) bool {
 	for _, slotRange := range *SlotRanges {
-		if slotRange.HasOverlap(&slotRange) {
+		if slotRange.HasOverlap(slotRange) {
 			return true
 		}
 	}
 	return false
 }
 
+func (s *SlotRange) Reset() {
+	s.Start = 0
+	s.Stop = 0
+}
+
+// FromSlotRange will return a MigratingSlot with the IsMigrating field set to true.
+// IsMigrating field would probably only be set to false from an unmarshal, like when
+// reading from the topology string
+func FromSlotRange(slotRange SlotRange) *MigratingSlot {
+	return &MigratingSlot{
+		SlotRange:   slotRange,
+		IsMigrating: true,
+	}
+}
+
+func (s *MigratingSlot) UnmarshalJSON(data []byte) error {
+	var slotsString any
+	if err := json.Unmarshal(data, &slotsString); err != nil {
+		return err
+	}
+	switch t := slotsString.(type) {
+	case string:
+		slotRange := SlotRange{}
+		err := json.Unmarshal(data, &slotRange)
+		if err != nil {
+			s.Reset()
+			return err
+		}
+		s.SlotRange = slotRange
+		s.IsMigrating = true
+	case float64:
+		// We use integer to represent the slot because we don't support the slot range
+		// in the past. So we need to support the integer type for backward compatibility.
+		// But the number in JSON is float64, so we need to convert it to int here.
+		if t == NotMigratingInt {
+			s.Reset()
+			return nil
+		}
+		if t < MinSlotID || t > MaxSlotID {
+			s.Reset()
+			return ErrSlotOutOfRange
+		}
+		slotID := int(t)
+		s.Start = slotID
+		s.Stop = slotID
+		s.IsMigrating = true
+	default:
+		s.Reset()
+		return fmt.Errorf("invalid slot range type: %T", slotsString)
+	}
+	return nil
+}
+
+func (s *MigratingSlot) MarshalJSON() ([]byte, error) {
+	if !s.IsMigrating {
+		// backwards compatibility. When we read from an old cluster that had `-1`
+		// denoting !isMigrating. The MigratingSlot field will not be nil. So when
+		// this field is marshal'd back into JSON format, we can keep it as it was
+		// which was `-1`.
+		// The only case this turns back to null is if a migration happens on this
+		// shard, and the function `ClearMigrateState()` is called on the shard.
+		return json.Marshal(NotMigratingInt)
+	}
+	return json.Marshal(s.String())
+}
+
+func (s *MigratingSlot) Reset() {
+	s.SlotRange.Reset()
+	s.IsMigrating = false
+}
+
 // CanMerge will return true if the given SlotRanges are adjacent with each other
 func CanMerge(a, b SlotRange) bool {
 	// Ensure a starts before b for easier comparison
@@ -218,7 +291,7 @@ func RemoveSlotFromSlotRanges(source SlotRanges, slot SlotRange) SlotRanges {
 	result := make([]SlotRange, 0, len(source))
 	for _, slotRange := range source {
 		// if no overlap, keep original range
-		if !slotRange.HasOverlap(&slot) {
+		if !slotRange.HasOverlap(slot) {
 			result = append(result, slotRange)
 			continue
 		}
diff --git a/store/slot_test.go b/store/slot_test.go
index 9158198..119ab09 100644
--- a/store/slot_test.go
+++ b/store/slot_test.go
@@ -20,6 +20,7 @@
 package store
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/apache/kvrocks-controller/consts"
@@ -42,6 +43,116 @@ func TestSlotRange_String(t *testing.T) {
 	assert.Equal(t, ErrSlotOutOfRange, err)
 }
 
+func TestMigratingSlot_UnmarshalJSON(t *testing.T) {
+	var migratingSlot MigratingSlot
+
+	migratingSlot = MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 5}, IsMigrating: true} // to set values to migratingSlot
+	slotBytes, err := json.Marshal(NotMigratingInt)
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &migratingSlot)
+	require.NoError(t, err, "expects no error since -1 was a valid 'not migrating' value")
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 0, Stop: 0}, false}, migratingSlot)
+
+	migratingSlot = MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 5}, IsMigrating: true} // to set values to migratingSlot
+	slotBytes, err = json.Marshal(-5)
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &migratingSlot)
+	require.ErrorIs(t, err, ErrSlotOutOfRange, "-5 is not a valid 'not migrating' value")
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 0, Stop: 0}, false}, migratingSlot)
+
+	slotBytes, err = json.Marshal("456")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 456, Stop: 456}, true}, migratingSlot)
+
+	slotBytes, err = json.Marshal("123-456")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 123, Stop: 456}, true}, migratingSlot)
+
+	slotBytes, err = json.Marshal("invalid-string")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &migratingSlot)
+	require.Error(t, err)
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 0, Stop: 0}, false}, migratingSlot)
+}
+
+// TestMigratingSlot_MarshalUnmarshalJSON will check that we can marshal and then unmarshal
+// back into the MigratingSlot
+func TestMigratingSlot_MarshalUnmarshalJSON(t *testing.T) {
+	migratingSlot := MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 5}, IsMigrating: true}
+	migratingSlotBytes, err := json.Marshal(&migratingSlot)
+	require.NoError(t, err)
+	err = json.Unmarshal(migratingSlotBytes, &migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 5, Stop: 5}, true}, migratingSlot)
+
+	// tests that we can marshal isMigrating = false, which results in -1, and then unmarshal it
+	// to be isMigrating = false again
+	migratingSlot = MigratingSlot{SlotRange: SlotRange{Start: 0, Stop: 0}, IsMigrating: false}
+	migratingSlotBytes, err = json.Marshal(&migratingSlot)
+	require.NoError(t, err)
+	err = json.Unmarshal(migratingSlotBytes, &migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 0, Stop: 0}, false}, migratingSlot)
+
+	// same test as earlier, but checks that it resets the start and stop
+	migratingSlot = MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 5}, IsMigrating: false}
+	migratingSlotBytes, err = json.Marshal(&migratingSlot)
+	require.NoError(t, err)
+	err = json.Unmarshal(migratingSlotBytes, &migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, MigratingSlot{SlotRange{Start: 0, Stop: 0}, false}, migratingSlot, "expects start and stop to reset to 0")
+}
+
+// TestMigratingSlot_MarshalJSON will checks the resulting string
+func TestMigratingSlot_MarshalJSON(t *testing.T) {
+	migratingSlot := MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 5}, IsMigrating: true}
+	migratingSlotBytes, err := json.Marshal(&migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, `"5"`, string(migratingSlotBytes))
+
+	migratingSlot = MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 10}, IsMigrating: true}
+	migratingSlotBytes, err = json.Marshal(&migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, `"5-10"`, string(migratingSlotBytes))
+
+	migratingSlot = MigratingSlot{SlotRange: SlotRange{Start: 5, Stop: 10}, IsMigrating: false}
+	migratingSlotBytes, err = json.Marshal(&migratingSlot)
+	require.NoError(t, err)
+	assert.Equal(t, `-1`, string(migratingSlotBytes))
+}
+
+func TestMigrateSlotRange_MarshalAndUnmarshalJSON(t *testing.T) {
+	var slotRange SlotRange
+
+	slotBytes, err := json.Marshal("-100")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &slotRange)
+	require.NotNil(t, err, "expects error since input is a negative number")
+	assert.Equal(t, SlotRange{Start: 0, Stop: 0}, slotRange)
+
+	slotBytes, err = json.Marshal("-100-100000")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &slotRange)
+	require.NotNil(t, err, "expects error since input is out of range")
+	assert.Equal(t, SlotRange{Start: 0, Stop: 0}, slotRange)
+
+	slotBytes, err = json.Marshal("456")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &slotRange)
+	require.NoError(t, err)
+	assert.Equal(t, SlotRange{Start: 456, Stop: 456}, slotRange)
+
+	slotBytes, err = json.Marshal("123-456")
+	require.NoError(t, err)
+	err = json.Unmarshal(slotBytes, &slotRange)
+	require.NoError(t, err)
+	assert.Equal(t, SlotRange{Start: 123, Stop: 456}, slotRange)
+}
+
 func TestSlotRange_Parse(t *testing.T) {
 	sr, err := ParseSlotRange("1-12")
 	assert.Nil(t, err)
@@ -82,31 +193,31 @@ func TestAddSlotToSlotRanges(t *testing.T) {
 	}
 	slotRange, err := NewSlotRange(0, 0)
 	require.NoError(t, err)
-	slotRanges = AddSlotToSlotRanges(slotRanges, *slotRange)
+	slotRanges = AddSlotToSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 3, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 0, Stop: 20}, slotRanges[0], slotRanges)
 
 	slotRange, err = NewSlotRange(21, 21)
 	require.NoError(t, err)
-	slotRanges = AddSlotToSlotRanges(slotRanges, *slotRange)
+	slotRanges = AddSlotToSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 3, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 0, Stop: 21}, slotRanges[0], slotRanges)
 
 	slotRange, err = NewSlotRange(50, 50)
 	require.NoError(t, err)
-	slotRanges = AddSlotToSlotRanges(slotRanges, *slotRange)
+	slotRanges = AddSlotToSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 50, Stop: 50}, slotRanges[1], slotRanges)
 
 	slotRange, err = NewSlotRange(200, 200)
 	require.NoError(t, err)
-	slotRanges = AddSlotToSlotRanges(slotRanges, *slotRange)
+	slotRanges = AddSlotToSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 3, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 101, Stop: 300}, slotRanges[2], slotRanges)
 
 	slotRange, err = NewSlotRange(400, 400)
 	require.NoError(t, err)
-	slotRanges = AddSlotToSlotRanges(slotRanges, *slotRange)
+	slotRanges = AddSlotToSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 400, Stop: 400}, slotRanges[3], slotRanges)
 }
@@ -119,56 +230,56 @@ func TestRemoveSlotRanges(t *testing.T) {
 	}
 	slotRange, err := NewSlotRange(0, 0)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 3, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 1, Stop: 20}, slotRanges[0], slotRanges)
 
 	slotRange, err = NewSlotRange(21, 21)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 3, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 1, Stop: 20}, slotRanges[0], slotRanges)
 
 	slotRange, err = NewSlotRange(20, 20)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 3, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 1, Stop: 19}, slotRanges[0], slotRanges)
 
 	slotRange, err = NewSlotRange(150, 150)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 101, Stop: 149}, slotRanges[1], slotRanges)
 
 	slotRange, err = NewSlotRange(101, 101)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 102, Stop: 149}, slotRanges[1], slotRanges)
 
 	slotRange, err = NewSlotRange(199, 199)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 151, Stop: 198}, slotRanges[2], slotRanges)
 
 	slotRange, err = NewSlotRange(300, 300)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 201, Stop: 299}, slotRanges[3], slotRanges)
 
 	slotRange, err = NewSlotRange(298, 298)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 5, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 201, Stop: 297}, slotRanges[3], slotRanges)
 	require.EqualValues(t, SlotRange{Start: 299, Stop: 299}, slotRanges[4], slotRanges)
 
 	slotRange, err = NewSlotRange(299, 299)
 	require.NoError(t, err)
-	slotRanges = RemoveSlotFromSlotRanges(slotRanges, *slotRange)
+	slotRanges = RemoveSlotFromSlotRanges(slotRanges, slotRange)
 	require.Equal(t, 4, len(slotRanges), slotRanges)
 	require.EqualValues(t, SlotRange{Start: 201, Stop: 297}, slotRanges[3], slotRanges)
 }
@@ -187,7 +298,7 @@ func TestSlotRange_HasOverlap(t *testing.T) {
 		Stop  int
 	}
 	type args struct {
-		that *SlotRange
+		that SlotRange
 	}
 	tests := []struct {
 		name   string
@@ -198,43 +309,43 @@ func TestSlotRange_HasOverlap(t *testing.T) {
 		{
 			name:   "0-5 does not overlap 6-7",
 			fields: fields{Start: 0, Stop: 5},
-			args:   args{&SlotRange{Start: 6, Stop: 7}},
+			args:   args{SlotRange{Start: 6, Stop: 7}},
 			want:   false,
 		},
 		{
 			name:   "0-5 does overlap 3-4",
 			fields: fields{Start: 0, Stop: 5},
-			args:   args{&SlotRange{Start: 3, Stop: 4}},
+			args:   args{SlotRange{Start: 3, Stop: 4}},
 			want:   true,
 		},
 		{
 			name:   "0-5 does overlap 5-8",
 			fields: fields{Start: 0, Stop: 5},
-			args:   args{&SlotRange{Start: 5, Stop: 8}},
+			args:   args{SlotRange{Start: 5, Stop: 8}},
 			want:   true,
 		},
 		{
 			name:   "0-5 does overlap 4-8",
 			fields: fields{Start: 0, Stop: 5},
-			args:   args{&SlotRange{Start: 4, Stop: 8}},
+			args:   args{SlotRange{Start: 4, Stop: 8}},
 			want:   true,
 		},
 		{
 			name:   "0-100 does not overlap 101-150",
 			fields: fields{Start: 0, Stop: 100},
-			args:   args{&SlotRange{Start: 101, Stop: 150}},
+			args:   args{SlotRange{Start: 101, Stop: 150}},
 			want:   false,
 		},
 		{
 			name:   "50-100 does overlap 30-50",
 			fields: fields{Start: 50, Stop: 100},
-			args:   args{&SlotRange{Start: 30, Stop: 50}},
+			args:   args{SlotRange{Start: 30, Stop: 50}},
 			want:   true,
 		},
 		{
 			name:   "50-100 does overlap 50-51",
 			fields: fields{Start: 50, Stop: 100},
-			args:   args{&SlotRange{Start: 50, Stop: 51}},
+			args:   args{SlotRange{Start: 50, Stop: 51}},
 			want:   true,
 		},
 	}