store/slot.go (200 lines of code) (raw):

/* * 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 store import ( "encoding/json" "errors" "fmt" "sort" "strconv" "strings" "github.com/apache/kvrocks-controller/consts" ) const ( MinSlotID = 0 MaxSlotID = 16383 ) var ErrSlotOutOfRange = errors.New("slot id was out of range, should be between 0 and 16383") type SlotRange struct { Start int `json:"start"` // inclusive Stop int `json:"stop"` // inclusive } type SlotRanges []SlotRange func NewSlotRange(start, stop int) (*SlotRange, error) { if start > stop { return nil, errors.New("start was larger than stop") } if (start < MinSlotID || start > MaxSlotID) || (stop < MinSlotID || stop > MaxSlotID) { return nil, ErrSlotOutOfRange } return &SlotRange{ Start: start, Stop: stop, }, nil } func (slotRange *SlotRange) Equal(that *SlotRange) bool { if that == nil { return false } if slotRange.Start != that.Start { return false } if slotRange.Stop != that.Stop { return false } return true } func (slotRange *SlotRange) HasOverlap(that *SlotRange) bool { return slotRange.Stop >= that.Start && slotRange.Start <= that.Stop } func (slotRange *SlotRange) Contains(slot int) bool { return slot >= slotRange.Start && slot <= slotRange.Stop } func (slotRange *SlotRange) String() string { if slotRange.Start == slotRange.Stop { return strconv.Itoa(slotRange.Start) } return strconv.Itoa(slotRange.Start) + "-" + strconv.Itoa(slotRange.Stop) } func (slotRange *SlotRange) MarshalJSON() ([]byte, error) { return json.Marshal(slotRange.String()) } func (slotRange *SlotRange) UnmarshalJSON(data []byte) error { var slotsString string if err := json.Unmarshal(data, &slotsString); err != nil { return err } slotObject, err := ParseSlotRange(slotsString) if err != nil { return err } *slotRange = *slotObject return nil } func ParseSlotRange(s string) (*SlotRange, error) { numberOfRanges := strings.Count(s, "-") if numberOfRanges > 1 { return nil, fmt.Errorf("%w, cannot have more than one range", consts.ErrInvalidArgument) } index := strings.IndexByte(s, '-') if index == -1 { start, err := strconv.Atoi(s) if err != nil { return nil, err } if start < MinSlotID || start > MaxSlotID { return nil, ErrSlotOutOfRange } return &SlotRange{ Start: start, Stop: start, }, nil } start, err := strconv.Atoi(s[0:index]) if err != nil { return nil, err } stop, err := strconv.Atoi(s[index+1:]) if err != nil { return nil, err } if start > stop { return nil, errors.New("start slot id greater than stop slot id") } if (start < MinSlotID || start > MaxSlotID) || (stop < MinSlotID || stop > MaxSlotID) { return nil, ErrSlotOutOfRange } return &SlotRange{ Start: start, Stop: stop, }, nil } func (SlotRanges *SlotRanges) Contains(slot int) bool { for _, slotRange := range *SlotRanges { if slotRange.Contains(slot) { return true } } return false } func (SlotRanges *SlotRanges) HasOverlap(slotRange SlotRange) bool { for _, slotRange := range *SlotRanges { if slotRange.HasOverlap(&slotRange) { return true } } return 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 if a.Start > b.Start { a, b = b, a } // If the end of `a` is at least one less than the start of `b`, they can merge return a.Stop+1 >= b.Start } func MergeSlotRanges(a SlotRange, b SlotRange) SlotRange { return SlotRange{ Start: min(a.Start, b.Start), Stop: max(a.Stop, b.Stop), } } // Implemented following leetcode solution: // https://leetcode.com/problems/merge-intervals/solutions/1805268/go-clean-code-with-explanation-and-visual-10ms-100 func AddSlotToSlotRanges(source SlotRanges, slot SlotRange) SlotRanges { if len(source) == 0 { return append(source, slot) } source = append(source, slot) sort.Slice(source, func(i, j int) bool { return source[i].Start < source[j].Start }) mergedSlotRanges := make([]SlotRange, 0, len(source)) mergedSlotRanges = append(mergedSlotRanges, source[0]) for _, interval := range source[1:] { lastIntervalPos := len(mergedSlotRanges) - 1 lastInterval := mergedSlotRanges[lastIntervalPos] if CanMerge(lastInterval, interval) { mergedSlotRanges[lastIntervalPos] = MergeSlotRanges(interval, lastInterval) } else { mergedSlotRanges = append(mergedSlotRanges, interval) } } return mergedSlotRanges } func RemoveSlotFromSlotRanges(source SlotRanges, slot SlotRange) SlotRanges { sort.Slice(source, func(i, j int) bool { return source[i].Start < source[j].Start }) if !source.HasOverlap(slot) { return source } result := make([]SlotRange, 0, len(source)) for _, slotRange := range source { // if no overlap, keep original range if !slotRange.HasOverlap(&slot) { result = append(result, slotRange) continue } // if overlap, then we need to create a new left and right range if slotRange.Start < slot.Start { result = append(result, SlotRange{ Start: slotRange.Start, Stop: slot.Start - 1, }) } if slotRange.Stop > slot.Stop { result = append(result, SlotRange{ Start: slot.Stop + 1, Stop: slotRange.Stop, }) } } return result } func CalculateSlotRanges(n int) SlotRanges { var slots []SlotRange rangeSize := (MaxSlotID + 1) / n for i := 0; i < n; i++ { if i != n-1 { slots = append(slots, SlotRange{Start: i * rangeSize, Stop: (i+1)*rangeSize - 1}) } else { slots = append(slots, SlotRange{Start: i * rangeSize, Stop: MaxSlotID}) } } return slots }