Squashed 'livekit-server/' content from commit 154b4d26

git-subtree-dir: livekit-server
git-subtree-split: 154b4d26b769c68a03c096124094b97bf61a996f
This commit is contained in:
2026-06-25 14:35:28 +09:00
commit 0da97ebd21
339 changed files with 114111 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
package utils
import (
"sync"
"time"
)
func NewDebouncer(after time.Duration) *Debouncer {
return &Debouncer{
after: after,
}
}
type Debouncer struct {
mu sync.Mutex
after time.Duration
timer *time.Timer
}
func (d *Debouncer) Add(f func()) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.after, f)
}
func (d *Debouncer) SetDuration(after time.Duration) {
d.mu.Lock()
d.after = after
d.mu.Unlock()
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed 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 utils
import (
"errors"
"fmt"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/pion/interceptor"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
)
// Do a fuzzy find for a codec in the list of codecs
// Used for lookup up a codec in an existing list to find a match
func CodecParametersFuzzySearch(needle webrtc.RTPCodecParameters, haystack []webrtc.RTPCodecParameters) (webrtc.RTPCodecParameters, error) {
// First attempt to match on MimeType + SDPFmtpLine
for _, c := range haystack {
if mime.IsMimeTypeStringEqual(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) &&
c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine {
return c, nil
}
}
// Fallback to just MimeType
for _, c := range haystack {
if mime.IsMimeTypeStringEqual(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) {
return c, nil
}
}
return webrtc.RTPCodecParameters{}, webrtc.ErrCodecNotFound
}
// Given a CodecParameters find the RTX CodecParameters if one exists
func FindRTXPayloadType(needle webrtc.PayloadType, haystack []webrtc.RTPCodecParameters) webrtc.PayloadType {
aptStr := fmt.Sprintf("apt=%d", needle)
for _, c := range haystack {
if aptStr == c.SDPFmtpLine {
return c.PayloadType
}
}
return webrtc.PayloadType(0)
}
// GetHeaderExtensionID returns the ID of a header extension, or 0 if not found
func GetHeaderExtensionID(extensions []interceptor.RTPHeaderExtension, extension webrtc.RTPHeaderExtensionCapability) int {
for _, h := range extensions {
if extension.URI == h.URI {
return h.ID
}
}
return 0
}
var (
ErrInvalidRTPVersion = errors.New("invalid RTP version")
ErrRTPPayloadTypeMismatch = errors.New("RTP payload type mismatch")
ErrRTPSSRCMismatch = errors.New("RTP SSRC mismatch")
)
// ValidateRTPPacket checks for a valid RTP packet and returns an error if fields are incorrect
func ValidateRTPPacket(pkt *rtp.Packet, expectedPayloadType uint8, expectedSSRC uint32) error {
if pkt.Version != 2 {
return fmt.Errorf("%w, expected: 2, actual: %d", ErrInvalidRTPVersion, pkt.Version)
}
if expectedPayloadType != 0 && pkt.PayloadType != expectedPayloadType {
return fmt.Errorf("%w, expected: %d, actual: %d", ErrRTPPayloadTypeMismatch, expectedPayloadType, pkt.PayloadType)
}
if expectedSSRC != 0 && pkt.SSRC != expectedSSRC {
return fmt.Errorf("%w, expected: %d, actual: %d", ErrRTPSSRCMismatch, expectedSSRC, pkt.SSRC)
}
return nil
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed 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 utils
import (
"time"
"go.uber.org/zap/zapcore"
)
type OWDEstimatorParams struct {
PropagationDelayFallFactor float64
PropagationDelayRiseFactor float64
PropagationDelaySpikeAdaptationFactor float64
PropagationDelayDeltaThresholdMin time.Duration
PropagationDelayDeltaThresholdMaxFactor int64
PropagationDelayDeltaHighResetNumReports int
PropagationDelayDeltaHighResetWait time.Duration
PropagationDelayDeltaLongTermAdaptationThreshold time.Duration
}
var OWDEstimatorParamsDefault = OWDEstimatorParams{
// OWD (One-Way-Delay) Estimator is used to estimate propagation delay between sender and receicer.
// As they operate on different clock domains, it is not possible to get exact propagation delay easily.
// So, this module is an estimator using a simple approach explained below. It should not be used for
// things that require high accuracy.
//
// One example is RTCP Sender Reports getting re-based to SFU time base so that all subscriber side
// can have the same time base (i. e. SFU time base). To convert publisher side
// RTCP Sender Reports to SFU timebase, a propagation delay is maintained.
// propagation_delay = time_of_report_reception - ntp_timestamp_in_report
//
// Propagation delay is adapted continuously. If it falls, adapt quickly to the
// lower value as that could be the real propagation delay. If it rises, adapt slowly
// as it might be a temporary change or slow drift. See below for handling of high deltas
// which could be a result of a path change.
PropagationDelayFallFactor: 0.9,
PropagationDelayRiseFactor: 0.1,
PropagationDelaySpikeAdaptationFactor: 0.5,
// To account for path changes mid-stream, if the delta of the propagation delay is consistently higher, reset.
// Reset at whichever of the below happens later.
// 1. 10 seconds of persistent high delta.
// 2. at least 2 consecutive reports with high delta.
//
// A long term estimate of delta of propagation delay is maintained and delta propagation delay exceeding
// a factor of the long term estimate is considered a sharp increase. That will trigger the start of the
// path change condition and if it persists, propagation delay will be reset.
PropagationDelayDeltaThresholdMin: 10 * time.Millisecond,
PropagationDelayDeltaThresholdMaxFactor: 2,
PropagationDelayDeltaHighResetNumReports: 2,
PropagationDelayDeltaHighResetWait: 10 * time.Second,
PropagationDelayDeltaLongTermAdaptationThreshold: 50 * time.Millisecond,
}
type OWDEstimator struct {
params OWDEstimatorParams
initialized bool
lastSenderClockTimeNs int64
lastPropagationDelayNs int64
lastDeltaPropagationDelayNs int64
estimatedPropagationDelayNs int64
longTermDeltaPropagationDelayNs int64
propagationDelayDeltaHighCount int
propagationDelayDeltaHighStartTime time.Time
propagationDelaySpikeNs int64
}
func NewOWDEstimator(params OWDEstimatorParams) *OWDEstimator {
return &OWDEstimator{
params: params,
}
}
func (o *OWDEstimator) MarshalLogObject(e zapcore.ObjectEncoder) error {
if o != nil {
e.AddTime("lastSenderClockTimeNs", time.Unix(0, o.lastSenderClockTimeNs))
e.AddDuration("lastPropagationDelayNs", time.Duration(o.lastPropagationDelayNs))
e.AddDuration("lastDeltaPropagationDelayNs", time.Duration(o.lastDeltaPropagationDelayNs))
e.AddDuration("estimatedPropagationDelayNs", time.Duration(o.estimatedPropagationDelayNs))
e.AddDuration("longTermDeltaPropagationDelayNs", time.Duration(o.longTermDeltaPropagationDelayNs))
e.AddInt("propagationDelayDeltaHighCount", o.propagationDelayDeltaHighCount)
e.AddTime("propagationDelayDeltaHighStartTime", o.propagationDelayDeltaHighStartTime)
e.AddDuration("propagationDelaySpikeNs", time.Duration(o.propagationDelaySpikeNs))
}
return nil
}
func (o *OWDEstimator) Update(senderClockTimeNs int64, receiverClockTimeNs int64) (int64, bool) {
resetDelta := func() {
o.propagationDelayDeltaHighCount = 0
o.propagationDelayDeltaHighStartTime = time.Time{}
o.propagationDelaySpikeNs = 0
}
initPropagationDelay := func(pd int64) {
o.estimatedPropagationDelayNs = pd
o.longTermDeltaPropagationDelayNs = 0
resetDelta()
}
o.lastPropagationDelayNs = receiverClockTimeNs - senderClockTimeNs
if !o.initialized {
o.initialized = true
o.lastSenderClockTimeNs = senderClockTimeNs
initPropagationDelay(o.lastPropagationDelayNs)
return o.estimatedPropagationDelayNs, true
}
stepChange := false
o.lastDeltaPropagationDelayNs = o.lastPropagationDelayNs - o.estimatedPropagationDelayNs
// check for path changes, i. e. a step jump increase in propagation delay observed over time
if o.lastDeltaPropagationDelayNs > o.params.PropagationDelayDeltaThresholdMin.Nanoseconds() { // ignore small changes for path change consideration
if o.longTermDeltaPropagationDelayNs != 0 &&
o.lastDeltaPropagationDelayNs > o.longTermDeltaPropagationDelayNs*o.params.PropagationDelayDeltaThresholdMaxFactor {
o.propagationDelayDeltaHighCount++
if o.propagationDelayDeltaHighStartTime.IsZero() {
o.propagationDelayDeltaHighStartTime = time.Now()
}
if o.propagationDelaySpikeNs == 0 {
o.propagationDelaySpikeNs = o.lastPropagationDelayNs
} else {
o.propagationDelaySpikeNs += int64(o.params.PropagationDelaySpikeAdaptationFactor * float64(o.lastPropagationDelayNs-o.propagationDelaySpikeNs))
}
if o.propagationDelayDeltaHighCount >= o.params.PropagationDelayDeltaHighResetNumReports && time.Since(o.propagationDelayDeltaHighStartTime) >= o.params.PropagationDelayDeltaHighResetWait {
stepChange = true
initPropagationDelay(o.propagationDelaySpikeNs)
}
} else {
resetDelta()
}
} else {
resetDelta()
factor := o.params.PropagationDelayFallFactor
if o.lastPropagationDelayNs > o.estimatedPropagationDelayNs {
factor = o.params.PropagationDelayRiseFactor
}
o.estimatedPropagationDelayNs += int64(factor * float64(o.lastPropagationDelayNs-o.estimatedPropagationDelayNs))
}
if o.lastDeltaPropagationDelayNs < o.params.PropagationDelayDeltaLongTermAdaptationThreshold.Nanoseconds() {
if o.longTermDeltaPropagationDelayNs == 0 {
o.longTermDeltaPropagationDelayNs = o.lastDeltaPropagationDelayNs
} else {
// do not adapt to large +ve spikes, can happen when channel is congested and reports are delivered very late
// if the spike is in fact a path change, it will persist and handled by path change detection above
sinceLast := senderClockTimeNs - o.lastSenderClockTimeNs
adaptationFactor := min(1.0, float64(sinceLast)/float64(o.params.PropagationDelayDeltaHighResetWait))
o.longTermDeltaPropagationDelayNs += int64(adaptationFactor * float64(o.lastDeltaPropagationDelayNs-o.longTermDeltaPropagationDelayNs))
}
}
if o.longTermDeltaPropagationDelayNs < 0 {
o.longTermDeltaPropagationDelayNs = 0
}
o.lastSenderClockTimeNs = senderClockTimeNs
return o.estimatedPropagationDelayNs, stepChange
}
func (o *OWDEstimator) EstimatedPropagationDelay() int64 {
return o.estimatedPropagationDelayNs
}
+203
View File
@@ -0,0 +1,203 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed 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 utils
import (
"errors"
"fmt"
"math"
"unsafe"
"go.uber.org/zap/zapcore"
)
const (
minRanges = 1
)
var (
errReversedOrder = errors.New("end <= start")
errKeyNotFound = errors.New("key not found")
errKeyTooOld = errors.New("key too old")
errKeyExcluded = errors.New("key excluded")
)
type rangeType interface {
uint32 | uint64
}
type valueType interface {
uint32 | uint64
}
// ---------------------------------------------------
type rangeVal[RT rangeType, VT valueType] struct {
start RT
end RT
value VT
}
func (r rangeVal[RT, VT]) MarshalLogObject(e zapcore.ObjectEncoder) error {
e.AddUint64("start", uint64(r.start))
e.AddUint64("end", uint64(r.end))
e.AddUint64("value", uint64(r.value))
return nil
}
// ---------------------------------------------------
type RangeMap[RT rangeType, VT valueType] struct {
halfRange RT
size int
ranges []rangeVal[RT, VT]
}
func NewRangeMap[RT rangeType, VT valueType](size int) *RangeMap[RT, VT] {
var t RT
r := &RangeMap[RT, VT]{
halfRange: 1 << ((unsafe.Sizeof(t) * 8) - 1),
size: int(math.Max(float64(size), float64(minRanges))),
}
r.initRanges(0, 0)
return r
}
func (r *RangeMap[RT, VT]) MarshalLogObject(e zapcore.ObjectEncoder) error {
e.AddInt("numRanges", len(r.ranges))
// just the last 10 ranges max
startIdx := len(r.ranges) - 10
if startIdx < 0 {
startIdx = 0
}
for i := startIdx; i < len(r.ranges); i++ {
e.AddObject(fmt.Sprintf("range[%d]", i), r.ranges[i])
}
return nil
}
func (r *RangeMap[RT, VT]) ClearAndResetValue(start RT, val VT) {
r.initRanges(start, val)
}
func (r *RangeMap[RT, VT]) DecValue(end RT, dec VT) {
lr := &r.ranges[len(r.ranges)-1]
if lr.start > end {
// modify existing value if end is in open range
lr.value -= dec
return
}
// close open range
lr.end = end
// start a new open one with decremented value
r.ranges = append(r.ranges, rangeVal[RT, VT]{
start: end + 1,
end: 0,
value: lr.value - dec,
})
r.prune()
}
func (r *RangeMap[RT, VT]) initRanges(start RT, val VT) {
r.ranges = []rangeVal[RT, VT]{
{
start: start,
end: 0,
value: val,
},
}
}
func (r *RangeMap[RT, VT]) ExcludeRange(startInclusive RT, endExclusive RT) error {
if endExclusive == startInclusive || endExclusive-startInclusive > r.halfRange {
return fmt.Errorf("%w, start %d, end %d", errReversedOrder, startInclusive, endExclusive)
}
lr := &r.ranges[len(r.ranges)-1]
if lr.start > startInclusive {
// start of open range is after start of exclusion range, cannot close the open range
return fmt.Errorf("%w, existingStart %d, newStart %d", errReversedOrder, lr.start, startInclusive)
}
newValue := lr.value + VT(endExclusive-startInclusive)
// if start of exclusion range matches start of open range, move the open range
if lr.start == startInclusive {
lr.start = endExclusive
lr.value = newValue
return nil
}
// close previous range
lr.end = startInclusive - 1
// start new open one after given exclusion range
r.ranges = append(r.ranges, rangeVal[RT, VT]{
start: endExclusive,
end: 0,
value: newValue,
})
r.prune()
return nil
}
func (r *RangeMap[RT, VT]) GetValue(key RT) (VT, error) {
numRanges := len(r.ranges)
if numRanges != 0 {
if key >= r.ranges[numRanges-1].start {
// in the open range
return r.ranges[numRanges-1].value, nil
}
if key < r.ranges[0].start {
// too old
return 0, errKeyTooOld
}
}
for idx := numRanges - 1; idx >= 0; idx-- {
rv := &r.ranges[idx]
if idx != numRanges-1 {
// open range checked above
if key-rv.start < r.halfRange && rv.end-key < r.halfRange {
return rv.value, nil
}
}
if idx > 0 {
rvPrev := &r.ranges[idx-1]
beforeDiff := key - rvPrev.end
afterDiff := rv.start - key
if beforeDiff > 0 && beforeDiff < r.halfRange && afterDiff > 0 && afterDiff < r.halfRange {
// in excluded range
return 0, errKeyExcluded
}
}
}
return 0, errKeyNotFound
}
func (r *RangeMap[RT, VT]) prune() {
if len(r.ranges) > r.size+1 { // +1 to accommodate the open range
r.ranges = r.ranges[len(r.ranges)-r.size-1:]
}
}
+361
View File
@@ -0,0 +1,361 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed 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 utils
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestRangeMapUint32(t *testing.T) {
r := NewRangeMap[uint32, uint32](2)
// getting value for any key should be 0 default
value, err := r.GetValue(33333)
require.NoError(t, err)
require.Equal(t, uint32(0), value)
expectedRanges := []rangeVal[uint32, uint32]{
{
start: 0,
end: 0,
value: 0,
},
}
require.Equal(t, expectedRanges, r.ranges)
// add an exclusion, should create a new range
err = r.ExcludeRange(10, 11)
require.NoError(t, err)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 0,
end: 9,
value: 0,
},
{
start: 11,
end: 0,
value: 1,
},
}
require.Equal(t, expectedRanges, r.ranges)
// getting value in old range should return 0
value, err = r.GetValue(6)
require.NoError(t, err)
require.Equal(t, uint32(0), value)
// newer should return 1
value, err = r.GetValue(11)
require.NoError(t, err)
require.Equal(t, uint32(1), value)
// excluded range should return error
value, err = r.GetValue(10)
require.ErrorIs(t, err, errKeyExcluded)
// out-of-order exclusion should return error
err = r.ExcludeRange(9, 10)
require.ErrorIs(t, err, errReversedOrder)
// flipped exclusion should return error
err = r.ExcludeRange(12, 11)
require.ErrorIs(t, err, errReversedOrder)
err = r.ExcludeRange(11, 11)
require.ErrorIs(t, err, errReversedOrder)
// add adjacent exclusion range of length = 1
err = r.ExcludeRange(11, 12)
require.NoError(t, err)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 0,
end: 9,
value: 0,
},
{
start: 12,
end: 0,
value: 2,
},
}
require.Equal(t, expectedRanges, r.ranges)
// excluded range should return error, now is excluded because exclusion range could be extended
value, err = r.GetValue(11)
require.ErrorIs(t, err, errKeyExcluded)
// getting value in old range should return 0
value, err = r.GetValue(6)
require.NoError(t, err)
// newer should return 2
value, err = r.GetValue(12)
require.NoError(t, err)
require.Equal(t, uint32(2), value)
// add adjacent exclusion range of length = 10
err = r.ExcludeRange(12, 22)
require.NoError(t, err)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 0,
end: 9,
value: 0,
},
{
start: 22,
end: 0,
value: 12,
},
}
require.Equal(t, expectedRanges, r.ranges)
// excluded range should return error, now is excluded because exclusion range could be extended
value, err = r.GetValue(15)
require.ErrorIs(t, err, errKeyExcluded)
// newer should return 12
value, err = r.GetValue(25)
require.NoError(t, err)
require.Equal(t, uint32(12), value)
// add a disjoint exclusion of length = 4
err = r.ExcludeRange(26, 30)
require.NoError(t, err)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 0,
end: 9,
value: 0,
},
{
start: 22,
end: 25,
value: 12,
},
{
start: 30,
end: 0,
value: 16,
},
}
require.Equal(t, expectedRanges, r.ranges)
// get a value from newly closed range [22, 25]
value, err = r.GetValue(23)
require.NoError(t, err)
require.Equal(t, uint32(12), value)
// add a disjoint exclusion of length = 1
err = r.ExcludeRange(50, 51)
require.NoError(t, err)
// previously first range would have been pruned due to size limitations
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 22,
end: 25,
value: 12,
},
{
start: 30,
end: 49,
value: 16,
},
{
start: 51,
end: 0,
value: 17,
},
}
require.Equal(t, expectedRanges, r.ranges)
// excluded range should return error
value, err = r.GetValue(50)
require.ErrorIs(t, err, errKeyExcluded)
value, err = r.GetValue(28)
require.ErrorIs(t, err, errKeyExcluded)
value, err = r.GetValue(17)
require.ErrorIs(t, err, errKeyTooOld)
// previously valid, but aged out key should return error
value, err = r.GetValue(5)
require.ErrorIs(t, err, errKeyTooOld)
// valid range access should return values
value, err = r.GetValue(24)
require.NoError(t, err)
require.Equal(t, uint32(12), value)
value, err = r.GetValue(34)
require.NoError(t, err)
require.Equal(t, uint32(16), value)
value, err = r.GetValue(49)
require.NoError(t, err)
require.Equal(t, uint32(16), value)
value, err = r.GetValue(55555555)
require.NoError(t, err)
require.Equal(t, uint32(17), value)
// reset
r.ClearAndResetValue(24, 23)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 24,
end: 0,
value: 23,
},
}
require.Equal(t, expectedRanges, r.ranges)
value, err = r.GetValue(55555555)
require.NoError(t, err)
require.Equal(t, uint32(23), value)
// decrement value and ensure that any key after start in ClearAndResetValue above returns that value
// (as given end is higher than open range start, open range should be closed and a new range added)
r.DecValue(34, 12)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 24,
end: 34,
value: 23,
},
{
start: 35,
end: 0,
value: 11,
},
}
require.Equal(t, expectedRanges, r.ranges)
value, err = r.GetValue(55555555)
require.NoError(t, err)
require.Equal(t, uint32(11), value)
// add an exclusion and then decrement value
err = r.ExcludeRange(40, 45)
require.NoError(t, err)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 24,
end: 34,
value: 23,
},
{
start: 35,
end: 39,
value: 11,
},
{
start: 45,
end: 0,
value: 16,
},
}
require.Equal(t, expectedRanges, r.ranges)
// before first range access
value, err = r.GetValue(5)
require.ErrorIs(t, err, errKeyTooOld)
// first range access
value, err = r.GetValue(25)
require.NoError(t, err)
require.Equal(t, uint32(23), value)
// second range access
value, err = r.GetValue(35)
require.NoError(t, err)
require.Equal(t, uint32(11), value)
// open range access
value, err = r.GetValue(55555555)
require.NoError(t, err)
require.Equal(t, uint32(16), value)
r.DecValue(66, 6)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 35,
end: 39,
value: 11,
},
{
start: 45,
end: 66,
value: 16,
},
{
start: 67,
end: 0,
value: 10,
},
}
require.Equal(t, expectedRanges, r.ranges)
// aged out range access
value, err = r.GetValue(25)
require.ErrorIs(t, err, errKeyTooOld)
// access closed range before decrementing value
value, err = r.GetValue(66)
require.NoError(t, err)
require.Equal(t, uint32(16), value)
// open range access
value, err = r.GetValue(67)
require.NoError(t, err)
require.Equal(t, uint32(10), value)
// decrement with old end and check that open range gets decremented
r.DecValue(66, 6)
expectedRanges = []rangeVal[uint32, uint32]{
{
start: 35,
end: 39,
value: 11,
},
{
start: 45,
end: 66,
value: 16,
},
{
start: 67,
end: 0,
value: 4,
},
}
require.Equal(t, expectedRanges, r.ranges)
// open range access should get decremented value
value, err = r.GetValue(67)
require.NoError(t, err)
require.Equal(t, uint32(4), value)
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed 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 utils
import (
"unsafe"
"go.uber.org/zap/zapcore"
)
type number interface {
uint16 | uint32
}
type extendedNumber interface {
uint32 | uint64
}
type WrapAroundParams struct {
IsRestartAllowed bool
}
type WrapAround[T number, ET extendedNumber] struct {
params WrapAroundParams
fullRange ET
initialized bool
start T
highest T
cycles ET
extendedHighest ET
}
func NewWrapAround[T number, ET extendedNumber](params WrapAroundParams) *WrapAround[T, ET] {
var t T
return &WrapAround[T, ET]{
params: params,
fullRange: 1 << (unsafe.Sizeof(t) * 8),
}
}
func (w *WrapAround[T, ET]) Seed(from *WrapAround[T, ET]) {
w.initialized = from.initialized
w.start = from.start
w.highest = from.highest
w.cycles = from.cycles
w.updateExtendedHighest()
}
type WrapAroundUpdateResult[ET extendedNumber] struct {
IsUnhandled bool // when set, other fields are invalid
IsRestart bool
PreExtendedStart ET // valid only if IsRestart = true
PreExtendedHighest ET
ExtendedVal ET
}
func (w *WrapAroundUpdateResult[ET]) MarshalLogObject(e zapcore.ObjectEncoder) error {
if w == nil {
return nil
}
e.AddBool("IsUnhandled", w.IsUnhandled)
e.AddBool("IsRestart", w.IsRestart)
e.AddUint64("PreExtendedStart", uint64(w.PreExtendedStart))
e.AddUint64("PreExtendedHighest", uint64(w.PreExtendedHighest))
e.AddUint64("ExtendedVal", uint64(w.ExtendedVal))
return nil
}
func (w *WrapAround[T, ET]) Update(val T) (result WrapAroundUpdateResult[ET]) {
if !w.initialized {
result.PreExtendedHighest = ET(val) - 1
result.ExtendedVal = ET(val)
w.start = val
w.highest = val
w.updateExtendedHighest()
w.initialized = true
return
}
gap := val - w.highest
if gap > T(w.fullRange>>1) {
// out-of-order
return w.maybeAdjustStart(val)
}
// in-order
result.PreExtendedHighest = w.extendedHighest
if val < w.highest {
w.cycles += w.fullRange
}
w.highest = val
w.updateExtendedHighest()
result.ExtendedVal = w.extendedHighest
return
}
func (w *WrapAround[T, ET]) UndoUpdate(result WrapAroundUpdateResult[ET]) {
if !w.initialized || result.PreExtendedHighest >= result.ExtendedVal {
return
}
w.ResetHighest(result.PreExtendedHighest)
}
func (w *WrapAround[T, ET]) Rollover(val T, numCycles int) (result WrapAroundUpdateResult[ET]) {
if numCycles < 0 || !w.initialized {
return w.Update(val)
}
result.PreExtendedHighest = w.extendedHighest
w.cycles += ET(numCycles) * w.fullRange
w.highest = val
w.updateExtendedHighest()
result.ExtendedVal = w.extendedHighest
return
}
func (w *WrapAround[T, ET]) RollbackRestart(ev ET) {
if w.isWrapBack(w.start, T(ev)) {
w.cycles -= w.fullRange
w.updateExtendedHighest()
}
w.start = T(ev)
}
func (w *WrapAround[T, ET]) ResetHighest(ev ET) {
w.highest = T(ev)
w.cycles = ev & ^(w.fullRange - 1)
w.updateExtendedHighest()
}
func (w *WrapAround[T, ET]) GetStart() T {
return w.start
}
func (w *WrapAround[T, ET]) GetExtendedStart() ET {
return ET(w.start)
}
func (w *WrapAround[T, ET]) GetHighest() T {
return w.highest
}
func (w *WrapAround[T, ET]) GetExtendedHighest() ET {
return w.extendedHighest
}
func (w *WrapAround[T, ET]) updateExtendedHighest() {
w.extendedHighest = getExtended(w.cycles, w.highest)
}
func (w *WrapAround[T, ET]) maybeAdjustStart(val T) (result WrapAroundUpdateResult[ET]) {
// re-adjust start if necessary. The conditions are
// 1. Not seen more than half the range yet
// 1. wrap back compared to start and not completed a half cycle, sequences like (10, 65530) in uint16 space
// 2. no wrap around, but out-of-order compared to start and not completed a half cycle , sequences like (10, 9), (65530, 65528) in uint16 space
cycles := w.cycles
totalNum := w.GetExtendedHighest() - w.GetExtendedStart() + 1
if totalNum > (w.fullRange >> 1) {
if w.isWrapBack(val, w.highest) {
cycles -= w.fullRange
}
result.PreExtendedHighest = w.extendedHighest
result.ExtendedVal = getExtended(cycles, val)
return
}
if val-w.start > T(w.fullRange>>1) {
if w.params.IsRestartAllowed {
// out-of-order with existing start => a new start
result.IsRestart = true
if val > w.start {
result.PreExtendedStart = w.fullRange + ET(w.start)
} else {
result.PreExtendedStart = ET(w.start)
}
if w.isWrapBack(val, w.highest) {
w.cycles = w.fullRange
w.updateExtendedHighest()
cycles = 0
}
w.start = val
} else {
result.IsUnhandled = true
}
} else {
if w.isWrapBack(val, w.highest) {
cycles -= w.fullRange
}
}
result.PreExtendedHighest = w.extendedHighest
result.ExtendedVal = getExtended(cycles, val)
return
}
func (w *WrapAround[T, ET]) isWrapBack(earlier T, later T) bool {
return ET(later) < (w.fullRange>>1) && ET(earlier) >= (w.fullRange>>1)
}
// ------------------------------------
func getExtended[T number, ET extendedNumber](cycles ET, val T) ET {
return cycles + ET(val)
}
+679
View File
@@ -0,0 +1,679 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed 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 utils
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestWrapAroundUint16(t *testing.T) {
w := NewWrapAround[uint16, uint32](WrapAroundParams{IsRestartAllowed: true})
testCases := []struct {
name string
input uint16
updated WrapAroundUpdateResult[uint32]
start uint16
extendedStart uint32
highest uint16
extendedHighest uint32
}{
// initialize
{
name: "initialize",
input: 10,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: 9,
ExtendedVal: 10,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// an older number without wrap around should reset start point
{
name: "reset start no wrap around",
input: 8,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: true,
PreExtendedStart: 10,
PreExtendedHighest: 10,
ExtendedVal: 8,
},
start: 8,
extendedStart: 8,
highest: 10,
extendedHighest: 10,
},
// an older number with wrap around should reset start point
{
name: "reset start wrap around",
input: (1 << 16) - 6,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: true,
PreExtendedStart: (1 << 16) + 8,
PreExtendedHighest: (1 << 16) + 10,
ExtendedVal: (1 << 16) - 6,
},
start: (1 << 16) - 6,
extendedStart: (1 << 16) - 6,
highest: 10,
extendedHighest: (1 << 16) + 10,
},
// an older number with wrap around should reset start point again
{
name: "reset start again",
input: (1 << 16) - 12,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: true,
PreExtendedStart: (1 << 16) - 6,
PreExtendedHighest: (1 << 16) + 10,
ExtendedVal: (1 << 16) - 12,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: 10,
extendedHighest: (1 << 16) + 10,
},
// out of order with highest, wrap back, but no restart
{
name: "out of order - no restart",
input: (1 << 16) - 3,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 16) + 10,
ExtendedVal: (1 << 16) - 3,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: 10,
extendedHighest: (1 << 16) + 10,
},
// duplicate should return same as highest
{
name: "duplicate",
input: 10,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 16) + 10,
ExtendedVal: (1 << 16) + 10,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: 10,
extendedHighest: (1 << 16) + 10,
},
// a significant jump in order should not reset start
{
name: "big in-order jump",
input: (1 << 15) - 10,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 16) + 10,
ExtendedVal: (1 << 16) + (1 << 15) - 10,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: (1 << 15) - 10,
extendedHighest: (1 << 16) + (1 << 15) - 10,
},
// now out-of-order should not reset start as half the range has been seen
{
name: "out-of-order after half range",
input: (1 << 15) - 11,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 16) + (1 << 15) - 10,
ExtendedVal: (1 << 16) + (1 << 15) - 11,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: (1 << 15) - 10,
extendedHighest: (1 << 16) + (1 << 15) - 10,
},
// wrap back out-of-order
{
name: "wrap back out-of-order after half range",
input: (1 << 16) - 1,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 16) + (1 << 15) - 10,
ExtendedVal: (1 << 16) - 1,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: (1 << 15) - 10,
extendedHighest: (1 << 16) + (1 << 15) - 10,
},
// in-order, should update highest
{
name: "in-order",
input: (1 << 15) + 3,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 16) + (1 << 15) - 10,
ExtendedVal: (1 << 16) + (1 << 15) + 3,
},
start: (1 << 16) - 12,
extendedStart: (1 << 16) - 12,
highest: (1 << 15) + 3,
extendedHighest: (1 << 16) + (1 << 15) + 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.updated, w.Update(tc.input))
require.Equal(t, tc.start, w.GetStart())
require.Equal(t, tc.extendedStart, w.GetExtendedStart())
require.Equal(t, tc.highest, w.GetHighest())
require.Equal(t, tc.extendedHighest, w.GetExtendedHighest())
})
}
}
func TestWrapAroundUint16NoRestart(t *testing.T) {
w := NewWrapAround[uint16, uint32](WrapAroundParams{IsRestartAllowed: false})
testCases := []struct {
name string
input uint16
updated WrapAroundUpdateResult[uint32]
start uint16
extendedStart uint32
highest uint16
extendedHighest uint32
}{
// initialize
{
name: "initialize",
input: 10,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: 9,
ExtendedVal: 10,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// an older number without wrap around should not reset start point
{
name: "no reset start no wrap around",
input: 8,
updated: WrapAroundUpdateResult[uint32]{
IsUnhandled: true,
// the following fields are not valid when `IsUnhandled = true`, but code fills it in
// and they are filled in here for testing purposes
PreExtendedHighest: 10,
ExtendedVal: 8,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// an older number with wrap around should not reset start point
{
name: "no reset start wrap around",
input: (1 << 16) - 6,
updated: WrapAroundUpdateResult[uint32]{
IsUnhandled: true,
PreExtendedHighest: 10,
ExtendedVal: (1 << 16) - 6,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// yet another older number with wrap around should not reset start point
{
name: "no reset start again",
input: (1 << 16) - 12,
updated: WrapAroundUpdateResult[uint32]{
IsUnhandled: true,
PreExtendedHighest: 10,
ExtendedVal: (1 << 16) - 12,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// duplicate should return same as highest
{
name: "duplicate",
input: 10,
updated: WrapAroundUpdateResult[uint32]{
PreExtendedHighest: 10,
ExtendedVal: 10,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// a significant jump in order should move highest to that
{
name: "big in-order jump",
input: (1 << 15) - 10,
updated: WrapAroundUpdateResult[uint32]{
PreExtendedHighest: 10,
ExtendedVal: (1 << 15) - 10,
},
start: 10,
extendedStart: 10,
highest: (1 << 15) - 10,
extendedHighest: (1 << 15) - 10,
},
// in-order, should update highest
{
name: "in-order",
input: (1 << 15) + 13,
updated: WrapAroundUpdateResult[uint32]{
PreExtendedHighest: (1 << 15) - 10,
ExtendedVal: (1 << 15) + 13,
},
start: 10,
extendedStart: 10,
highest: (1 << 15) + 13,
extendedHighest: (1 << 15) + 13,
},
// now out-of-order should not reset start as half the range has been seen
{
name: "out-of-order after half range",
input: (1 << 15) - 11,
updated: WrapAroundUpdateResult[uint32]{
PreExtendedHighest: (1 << 15) + 13,
ExtendedVal: (1 << 15) - 11,
},
start: 10,
extendedStart: 10,
highest: (1 << 15) + 13,
extendedHighest: (1 << 15) + 13,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.updated, w.Update(tc.input))
require.Equal(t, tc.start, w.GetStart())
require.Equal(t, tc.extendedStart, w.GetExtendedStart())
require.Equal(t, tc.highest, w.GetHighest())
require.Equal(t, tc.extendedHighest, w.GetExtendedHighest())
})
}
}
func TestWrapAroundUint16RollbackRestartAndResetHighest(t *testing.T) {
w := NewWrapAround[uint16, uint64](WrapAroundParams{IsRestartAllowed: true})
// initialize
w.Update(23)
require.Equal(t, uint16(23), w.GetStart())
require.Equal(t, uint64(23), w.GetExtendedStart())
require.Equal(t, uint16(23), w.GetHighest())
require.Equal(t, uint64(23), w.GetExtendedHighest())
// an in-order update
w.Update(25)
require.Equal(t, uint16(23), w.GetStart())
require.Equal(t, uint64(23), w.GetExtendedStart())
require.Equal(t, uint16(25), w.GetHighest())
require.Equal(t, uint64(25), w.GetExtendedHighest())
// force restart without wrap
res := w.Update(12)
expectedResult := WrapAroundUpdateResult[uint64]{
IsRestart: true,
PreExtendedStart: 23,
PreExtendedHighest: 25,
ExtendedVal: 12,
}
require.Equal(t, expectedResult, res)
require.Equal(t, uint16(12), w.GetStart())
require.Equal(t, uint64(12), w.GetExtendedStart())
require.Equal(t, uint16(25), w.GetHighest())
require.Equal(t, uint64(25), w.GetExtendedHighest())
// roll back restart
w.RollbackRestart(res.PreExtendedStart)
require.Equal(t, uint16(23), w.GetStart())
require.Equal(t, uint64(23), w.GetExtendedStart())
require.Equal(t, uint16(25), w.GetHighest())
require.Equal(t, uint64(25), w.GetExtendedHighest())
// force restart with wrap
res = w.Update(65533)
expectedResult = WrapAroundUpdateResult[uint64]{
IsRestart: true,
PreExtendedStart: (1 << 16) + 23,
PreExtendedHighest: (1 << 16) + 25,
ExtendedVal: 65533,
}
require.Equal(t, expectedResult, res)
require.Equal(t, uint16(65533), w.GetStart())
require.Equal(t, uint64(65533), w.GetExtendedStart())
require.Equal(t, uint16(25), w.GetHighest())
require.Equal(t, uint64(65536+25), w.GetExtendedHighest())
// roll back restart
w.RollbackRestart(res.PreExtendedStart)
require.Equal(t, uint16(23), w.GetStart())
require.Equal(t, uint64(23), w.GetExtendedStart())
require.Equal(t, uint16(25), w.GetHighest())
require.Equal(t, uint64(25), w.GetExtendedHighest())
// reset highest
w.ResetHighest(0x1234)
require.Equal(t, uint16(23), w.GetStart())
require.Equal(t, uint64(23), w.GetExtendedStart())
require.Equal(t, uint16(0x1234), w.GetHighest())
require.Equal(t, uint64(0x1234), w.GetExtendedHighest())
w.ResetHighest(0x7f1234)
require.Equal(t, uint16(23), w.GetStart())
require.Equal(t, uint64(23), w.GetExtendedStart())
require.Equal(t, uint16(0x1234), w.GetHighest())
require.Equal(t, uint64(0x7f1234), w.GetExtendedHighest())
}
func TestWrapAroundUint16WrapAroundRestartDuplicate(t *testing.T) {
w := NewWrapAround[uint16, uint64](WrapAroundParams{IsRestartAllowed: true})
// initialize
w.Update(65534)
require.Equal(t, uint16(65534), w.GetStart())
require.Equal(t, uint64(65534), w.GetExtendedStart())
require.Equal(t, uint16(65534), w.GetHighest())
require.Equal(t, uint64(65534), w.GetExtendedHighest())
// an in-order update with a roll over
w.Update(32)
require.Equal(t, uint16(65534), w.GetStart())
require.Equal(t, uint64(65534), w.GetExtendedStart())
require.Equal(t, uint16(32), w.GetHighest())
require.Equal(t, uint64(65568), w.GetExtendedHighest())
// duplicate of start
res := w.Update(65534)
expectedResult := WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: 65568,
ExtendedVal: 65534,
}
require.Equal(t, expectedResult, res)
require.Equal(t, uint16(65534), w.GetStart())
require.Equal(t, uint64(65534), w.GetExtendedStart())
require.Equal(t, uint16(32), w.GetHighest())
require.Equal(t, uint64(65568), w.GetExtendedHighest())
// duplicate of start - again
res = w.Update(65534)
expectedResult = WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: 65568,
ExtendedVal: 65534,
}
require.Equal(t, expectedResult, res)
require.Equal(t, uint16(65534), w.GetStart())
require.Equal(t, uint64(65534), w.GetExtendedStart())
require.Equal(t, uint16(32), w.GetHighest())
require.Equal(t, uint64(65568), w.GetExtendedHighest())
}
func TestWrapAroundUint16Rollover(t *testing.T) {
w := NewWrapAround[uint16, uint32](WrapAroundParams{IsRestartAllowed: false})
testCases := []struct {
name string
input uint16
numCycles int
updated WrapAroundUpdateResult[uint32]
start uint16
extendedStart uint32
highest uint16
extendedHighest uint32
}{
// initialize - should initialize irrespective of numCycles
{
name: "initialize",
input: 10,
numCycles: 10,
updated: WrapAroundUpdateResult[uint32]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: 9,
ExtendedVal: 10,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// negative cycles - should just do an update
{
name: "zero",
input: 8,
numCycles: -1,
updated: WrapAroundUpdateResult[uint32]{
IsUnhandled: true,
// the following fields are not valid when `IsUnhandled = true`, but code fills it in
// and they are filled in here for testing purposes
PreExtendedHighest: 10,
ExtendedVal: 8,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// one cycle
{
name: "one cycle",
input: (1 << 16) - 6,
numCycles: 1,
updated: WrapAroundUpdateResult[uint32]{
PreExtendedHighest: 10,
ExtendedVal: (1 << 16) - 6 + (1 << 16),
},
start: 10,
extendedStart: 10,
highest: (1 << 16) - 6,
extendedHighest: (1 << 16) - 6 + (1 << 16),
},
// two cycles
{
name: "two cycles",
input: (1 << 16) - 7,
numCycles: 2,
updated: WrapAroundUpdateResult[uint32]{
PreExtendedHighest: (1 << 16) - 6 + (1 << 16),
ExtendedVal: (1 << 16) - 7 + 3*(1<<16),
},
start: 10,
extendedStart: 10,
highest: (1 << 16) - 7,
extendedHighest: (1 << 16) - 7 + 3*(1<<16),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.updated, w.Rollover(tc.input, tc.numCycles))
require.Equal(t, tc.start, w.GetStart())
require.Equal(t, tc.extendedStart, w.GetExtendedStart())
require.Equal(t, tc.highest, w.GetHighest())
require.Equal(t, tc.extendedHighest, w.GetExtendedHighest())
})
}
}
func TestWrapAroundUint32(t *testing.T) {
w := NewWrapAround[uint32, uint64](WrapAroundParams{IsRestartAllowed: true})
testCases := []struct {
name string
input uint32
updated WrapAroundUpdateResult[uint64]
start uint32
extendedStart uint64
highest uint32
extendedHighest uint64
}{
// initialize
{
name: "initialize",
input: 10,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: 9,
ExtendedVal: 10,
},
start: 10,
extendedStart: 10,
highest: 10,
extendedHighest: 10,
},
// an older number without wrap around should reset start point
{
name: "reset start no wrap around",
input: 8,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: true,
PreExtendedStart: 10,
PreExtendedHighest: 10,
ExtendedVal: 8,
},
start: 8,
extendedStart: 8,
highest: 10,
extendedHighest: 10,
},
// an older number with wrap around should reset start point
{
name: "reset start wrap around",
input: (1 << 32) - 6,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: true,
PreExtendedStart: (1 << 32) + 8,
PreExtendedHighest: (1 << 32) + 10,
ExtendedVal: (1 << 32) - 6,
},
start: (1 << 32) - 6,
extendedStart: (1 << 32) - 6,
highest: 10,
extendedHighest: (1 << 32) + 10,
},
// an older number with wrap around should reset start point again
{
name: "reset start again",
input: (1 << 32) - 12,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: true,
PreExtendedStart: (1 << 32) - 6,
PreExtendedHighest: (1 << 32) + 10,
ExtendedVal: (1 << 32) - 12,
},
start: (1 << 32) - 12,
extendedStart: (1 << 32) - 12,
highest: 10,
extendedHighest: (1 << 32) + 10,
},
// duplicate should return same as highest
{
name: "duplicate",
input: 10,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 32) + 10,
ExtendedVal: (1 << 32) + 10,
},
start: (1 << 32) - 12,
extendedStart: (1 << 32) - 12,
highest: 10,
extendedHighest: (1 << 32) + 10,
},
// a significant jump in order should not reset start
{
name: "big in-order jump",
input: 1 << 31,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 32) + 10,
ExtendedVal: (1 << 32) + (1 << 31),
},
start: (1 << 32) - 12,
extendedStart: (1 << 32) - 12,
highest: 1 << 31,
extendedHighest: (1 << 32) + (1 << 31),
},
// now out-of-order should not reset start as half the range has been seen
{
name: "out-of-order after half range",
input: (1 << 31) - 1,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 32) + (1 << 31),
ExtendedVal: (1 << 32) + (1 << 31) - 1,
},
start: (1 << 32) - 12,
extendedStart: (1 << 32) - 12,
highest: 1 << 31,
extendedHighest: (1 << 32) + (1 << 31),
},
// in-order, should update highest
{
name: "in-order",
input: (1 << 31) + 3,
updated: WrapAroundUpdateResult[uint64]{
IsRestart: false,
PreExtendedStart: 0,
PreExtendedHighest: (1 << 32) + (1 << 31),
ExtendedVal: (1 << 32) + (1 << 31) + 3,
},
start: (1 << 32) - 12,
extendedStart: (1 << 32) - 12,
highest: (1 << 31) + 3,
extendedHighest: (1 << 32) + (1 << 31) + 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.updated, w.Update(tc.input))
require.Equal(t, tc.start, w.GetStart())
require.Equal(t, tc.extendedStart, w.GetExtendedStart())
require.Equal(t, tc.highest, w.GetHighest())
require.Equal(t, tc.extendedHighest, w.GetExtendedHighest())
})
}
}