Generics for clocks

drew/mqtt-clients
Drew Bednar 4 months ago
parent e05c1fd788
commit c2cc2e6a81

@ -2,9 +2,18 @@ package clock
import ( import (
"errors" "errors"
"fmt"
) )
func max(x, y uint) uint { type Uint interface {
uint32 | uint64
}
type VectorClock[T Uint] struct {
clock []T
}
func max[T Uint](x, y T) T {
if x >= y { if x >= y {
return x return x
} else { } else {
@ -12,11 +21,6 @@ func max(x, y uint) uint {
} }
} }
// VectorClock type is used for determining the partial ordering of events in a distributed system.
type VectorClock struct {
clock []uint
}
// GetClock returns a copy of the internal vector clock. // GetClock returns a copy of the internal vector clock.
// //
// This method provides a snapshot of the current state of the vector clock // This method provides a snapshot of the current state of the vector clock
@ -26,15 +30,36 @@ type VectorClock struct {
// //
// Returns: // Returns:
// //
// []int: A copy of the internal vector clock slice, where each element // []T: A copy of the internal vector clock slice, where each element
// represents the logical time for a corresponding process. // represents the logical time for a corresponding process. The type T
func (vc *VectorClock) GetClock() []uint { // is constrained by the Uint interface and can be either uint32 or uint64.
clock := make([]uint, len(vc.clock)) func (vc *VectorClock[T]) GetClock() []T {
clock := make([]T, len(vc.clock))
copy(clock, vc.clock) copy(clock, vc.clock)
return clock return clock
} }
func (vc *VectorClock) Sync(v VectorClock) ([]uint, error) { // Sync synchronizes the current VectorClock with another VectorClock of the same type.
//
// This method takes another VectorClock instance, compares the logical times for each
// process, and updates the current VectorClock to hold the maximum logical time for
// each process. The synchronization ensures that the resulting vector clock reflects
// the latest logical times for both clocks.
//
// If the lengths of the two VectorClocks differ, an error is returned.
//
// Parameters:
//
// v VectorClock[T]: The other VectorClock instance to synchronize with. It must be
// of the same type T (either uint32 or uint64), as constrained by the Uint interface.
//
// Returns:
//
// []T: A copy of the synchronized vector clock, where each element represents the
// updated logical time for the corresponding process.
//
// error: An error is returned if the two VectorClocks have different lengths.
func (vc *VectorClock[T]) Sync(v VectorClock[T]) ([]T, error) {
compClock := v.GetClock() compClock := v.GetClock()
if len(vc.clock) != len(compClock) { if len(vc.clock) != len(compClock) {
@ -48,24 +73,39 @@ func (vc *VectorClock) Sync(v VectorClock) ([]uint, error) {
return vc.GetClock(), nil return vc.GetClock(), nil
} }
// NewVectorClock creates a new VectorClock initialized to zero. func (vc *VectorClock[T]) Increment(index int) ([]T, error) {
if index > len(vc.clock) || vc.clock == nil {
return nil, errors.New(fmt.Sprintf("Cannot access index: %d, clock is of length %d", index, len(vc.clock)))
}
// TODO handle Uint overflow?
vc.clock[index] = vc.clock[index] + 1
return vc.GetClock(), nil
}
// NewVectorClock creates a new generic VectorClock initialized to zero.
//
// This function creates a new instance of VectorClock for the specified type T,
// which can be either uint32 or uint64, depending on the needs of the application.
// //
// Parameters: // Parameters:
// //
// size (int): The number of entries in the vector clock, typically // size (T): The number of entries in the vector clock, typically corresponding
// corresponding to the number of processes or nodes. // to the number of processes or nodes. The size is of type T, which can be uint32
// or uint64, as defined by the Uint constraint.
// //
// Returns: // Returns:
// //
// *VectorClock: A pointer to the newly created VectorClock instance, with all // *VectorClock[T]: A pointer to the newly created VectorClock instance, where all
// elements of the clock initialized to zero. // elements of the clock are initialized to zero.
func NewVectorClock(size uint) *VectorClock { func NewVectorClock[T Uint](size int) *VectorClock[T] {
return &VectorClock{ return &VectorClock[T]{
clock: make([]uint, size), clock: make([]T, size),
} }
} }
// ArrayToVectorClock converts an integer array into a VectorClock instance. // ArrayToVectorClock converts an array of type T into a VectorClock instance.
// //
// This function creates a new VectorClock where the internal clock is initialized // This function creates a new VectorClock where the internal clock is initialized
// by copying the values from the provided array. The input array's values are // by copying the values from the provided array. The input array's values are
@ -73,15 +113,17 @@ func NewVectorClock(size uint) *VectorClock {
// //
// Parameters: // Parameters:
// //
// a []uint: An array of integers representing logical times for each process. // a []T: An array of type T (either uint32 or uint64) representing logical
// times for each process. The type T is constrained by the Uint interface, which
// ensures it is either uint32 or uint64.
// //
// Returns: // Returns:
// //
// *VectorClock: A pointer to a newly created VectorClock instance where the // *VectorClock[T]: A pointer to a newly created VectorClock instance where the
// internal clock is a copy of the provided array. // internal clock is a copy of the provided array.
func ArrayToVectorClock(a []uint) *VectorClock { func ArrayToVectorClock[T Uint](a []T) *VectorClock[T] {
vc := &VectorClock{ vc := &VectorClock[T]{
clock: make([]uint, len(a)), clock: make([]T, len(a)),
} }
copy(vc.clock, a) copy(vc.clock, a)
return vc return vc

@ -1,11 +1,12 @@
package clock package clock
import ( import (
"math"
"reflect" "reflect"
"testing" "testing"
) )
func assertClocksEqual(t testing.TB, got, want []uint) { func assertClocksEqual[T Uint](t testing.TB, got, want []T) {
t.Helper() t.Helper()
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
@ -16,24 +17,51 @@ func assertClocksEqual(t testing.TB, got, want []uint) {
func TestVectorClock(t *testing.T) { func TestVectorClock(t *testing.T) {
t.Run("start value sync", func(t *testing.T) { t.Run("test cases for uint32 type", func(t *testing.T) {
testCases := []struct { testCases := []struct {
a []uint a []uint32
b []uint b []uint32
expected []uint expected []uint32
}{ }{
{[]uint{0, 0}, []uint{0, 0}, []uint{0, 0}}, {[]uint32{0, 0}, []uint32{0, 0}, []uint32{0, 0}},
{[]uint{2, 0}, []uint{0, 2}, []uint{2, 2}}, {[]uint32{2, 0}, []uint32{0, 2}, []uint32{2, 2}},
{[]uint{4, 11}, []uint{3, 10}, []uint{4, 11}}, {[]uint32{4, 11}, []uint32{3, 10}, []uint32{4, 11}},
{[]uint{5, 9}, []uint{8, 12}, []uint{8, 12}}, {[]uint32{5, 9}, []uint32{8, 12}, []uint32{8, 12}},
{[]uint{1, 1}, []uint{1, 1}, []uint{1, 1}}, {[]uint32{1, 1}, []uint32{1, 1}, []uint32{1, 1}},
// {[]uint{math.MaxUint32, 1}, []uint{(math.MaxUint32 + 1), 1}, []uint{1, 1}}, {[]uint32{math.MaxUint32, 1}, []uint32{2, 1}, []uint32{4294967295, 1}},
} }
for _, tc := range testCases { for _, tc := range testCases {
vc := VectorClock{clock: tc.a} vc := VectorClock[uint32]{clock: tc.a}
clock, err := vc.Sync(VectorClock{clock: tc.b}) clock, err := vc.Sync(VectorClock[uint32]{clock: tc.b})
if err != nil {
t.Errorf("Sync should not have errored")
}
assertClocksEqual(t, clock, tc.expected)
}
})
t.Run("test cases for uint64 type", func(t *testing.T) {
testCases := []struct {
a []uint64
b []uint64
expected []uint64
}{
{[]uint64{0, 0}, []uint64{0, 0}, []uint64{0, 0}},
{[]uint64{2, 0}, []uint64{0, 2}, []uint64{2, 2}},
{[]uint64{4, 11}, []uint64{3, 10}, []uint64{4, 11}},
{[]uint64{5, 9}, []uint64{8, 12}, []uint64{8, 12}},
{[]uint64{1, 1}, []uint64{1, 1}, []uint64{1, 1}},
{[]uint64{math.MaxUint64, 1}, []uint64{2, 1}, []uint64{18446744073709551615, 1}},
}
for _, tc := range testCases {
vc := VectorClock[uint64]{clock: tc.a}
clock, err := vc.Sync(VectorClock[uint64]{clock: tc.b})
if err != nil { if err != nil {
t.Errorf("Sync should not have errored") t.Errorf("Sync should not have errored")
@ -44,51 +72,89 @@ func TestVectorClock(t *testing.T) {
}) })
t.Run("basic sync 2", func(t *testing.T) { t.Run("test empty new", func(t *testing.T) {
vc := VectorClock{clock: []uint{4, 11}}
clock, err := vc.Sync(VectorClock{clock: []uint{4, 10}}) got := NewVectorClock[uint32](2)
want := VectorClock[uint32]{clock: []uint32{0, 0}}
assertClocksEqual(t, got.GetClock(), want.GetClock())
})
t.Run("test new vc for uint32", func(t *testing.T) {
vc := VectorClock[uint32]{clock: []uint32{4, 11}}
clock, err := vc.Increment(0)
if err != nil { if err != nil {
t.Errorf("Sync should not have errored") t.Errorf("Increment should not have errored")
} }
assertClocksEqual(t, clock, []uint{4, 11}) assertClocksEqual(t, clock, []uint32{5, 11})
}) clock, err = vc.Increment(1)
t.Run("basic sync", func(t *testing.T) { if err != nil {
t.Errorf("Increment should not have errored")
}
assertClocksEqual(t, clock, []uint32{5, 12})
}) })
t.Run("basic sync", func(t *testing.T) {
t.Run("test overflow condition", func(t *testing.T) {
vc := VectorClock[uint32]{clock: []uint32{math.MaxUint32, 11}}
clock, err := vc.Increment(0)
if err != nil {
t.Errorf("Increment should not have errored")
}
assertClocksEqual(t, clock, []uint32{0, 11})
}) })
vc := VectorClock{clock: []uint{2, 0}}
clock, err := vc.Sync(VectorClock{clock: []uint{0, 2}}) t.Run("test index cannot be incremented", func(t *testing.T) {
vc := VectorClock[uint64]{}
if err != nil { clock, err := vc.Increment(0)
t.Errorf("Sync should not have errored")
}
assertClocksEqual(t, clock, []uint{2, 2}) if clock != nil {
t.Errorf("Clock should be nil")
}
if err == nil {
t.Errorf("There should have been an error")
}
})
} }
func TestNewVectorClock(t *testing.T) { func TestNewVectorClock(t *testing.T) {
got := NewVectorClock(2) t.Run("test new vc for uint32", func(t *testing.T) {
want := VectorClock{clock: []uint{0, 0}}
got := NewVectorClock[uint32](2)
want := VectorClock[uint32]{clock: []uint32{0, 0}}
assertClocksEqual(t, got.GetClock(), want.GetClock())
})
t.Run("test new vc for uint64", func(t *testing.T) {
got := NewVectorClock[uint64](2)
want := VectorClock[uint64]{clock: []uint64{0, 0}}
assertClocksEqual(t, got.GetClock(), want.GetClock()) assertClocksEqual(t, got.GetClock(), want.GetClock())
})
} }
func TestArrayToVectorClock(t *testing.T) { func TestArrayToVectorClock(t *testing.T) {
t.Run("from empty slice", func(t *testing.T) { t.Run("from empty slice", func(t *testing.T) {
got := ArrayToVectorClock([]uint{}) got := ArrayToVectorClock([]uint32{})
want := VectorClock{ want := VectorClock[uint32]{
clock: []uint{}, clock: []uint32{},
} }
if len(got.GetClock()) != 0 { if len(got.GetClock()) != 0 {
@ -101,34 +167,34 @@ func TestArrayToVectorClock(t *testing.T) {
}) })
t.Run("from zero slice", func(t *testing.T) { t.Run("from zero slice", func(t *testing.T) {
got := ArrayToVectorClock(make([]uint, 6)) got := ArrayToVectorClock(make([]uint64, 6))
want := VectorClock{ want := VectorClock[uint64]{
clock: []uint{0, 0, 0, 0, 0, 0}, clock: []uint64{0, 0, 0, 0, 0, 0},
} }
assertClocksEqual(t, got.GetClock(), want.GetClock()) assertClocksEqual(t, got.GetClock(), want.GetClock())
}) })
t.Run("from start clock", func(t *testing.T) { t.Run("from start clock", func(t *testing.T) {
want := NewVectorClock(3) want := NewVectorClock[uint64](3)
got := ArrayToVectorClock(want.GetClock()) got := ArrayToVectorClock(want.GetClock())
assertClocksEqual(t, got.GetClock(), want.GetClock()) assertClocksEqual(t, got.GetClock(), want.GetClock())
}) })
t.Run("from a populated clock", func(t *testing.T) { t.Run("from a populated clock", func(t *testing.T) {
want := VectorClock{ want := VectorClock[uint32]{
clock: []uint{2, 3}, clock: []uint32{2, 3},
} }
got := ArrayToVectorClock(want.GetClock()) got := ArrayToVectorClock(want.GetClock())
assertClocksEqual(t, got.GetClock(), want.GetClock()) assertClocksEqual(t, got.GetClock(), want.GetClock())
}) })
t.Run("from a slice that is then modified", func(t *testing.T) { t.Run("from a slice that is then modified", func(t *testing.T) {
initial_a := []uint{1, 2, 3, 4} initial_a := []uint64{1, 2, 3, 4}
got := ArrayToVectorClock(initial_a) got := ArrayToVectorClock(initial_a)
want := make([]uint, len(initial_a)) want := make([]uint64, len(initial_a))
copy(want, initial_a) copy(want, initial_a)
initial_a[0] = 12 initial_a[0] = 12

Loading…
Cancel
Save