diff --git a/clock/clock.go b/clock/clock.go index 4ab1f65..aed52cc 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -2,9 +2,18 @@ package clock import ( "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 { return x } 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. // // This method provides a snapshot of the current state of the vector clock @@ -26,15 +30,36 @@ type VectorClock struct { // // Returns: // -// []int: A copy of the internal vector clock slice, where each element -// represents the logical time for a corresponding process. -func (vc *VectorClock) GetClock() []uint { - clock := make([]uint, len(vc.clock)) +// []T: A copy of the internal vector clock slice, where each element +// represents the logical time for a corresponding process. The type T +// is constrained by the Uint interface and can be either uint32 or uint64. +func (vc *VectorClock[T]) GetClock() []T { + clock := make([]T, len(vc.clock)) copy(clock, vc.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() if len(vc.clock) != len(compClock) { @@ -48,24 +73,39 @@ func (vc *VectorClock) Sync(v VectorClock) ([]uint, error) { 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: // -// size (int): The number of entries in the vector clock, typically -// corresponding to the number of processes or nodes. +// size (T): The number of entries in the vector clock, typically corresponding +// 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: // -// *VectorClock: A pointer to the newly created VectorClock instance, with all -// elements of the clock initialized to zero. -func NewVectorClock(size uint) *VectorClock { - return &VectorClock{ - clock: make([]uint, size), +// *VectorClock[T]: A pointer to the newly created VectorClock instance, where all +// elements of the clock are initialized to zero. +func NewVectorClock[T Uint](size int) *VectorClock[T] { + return &VectorClock[T]{ + 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 // by copying the values from the provided array. The input array's values are @@ -73,15 +113,17 @@ func NewVectorClock(size uint) *VectorClock { // // 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: // -// *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. -func ArrayToVectorClock(a []uint) *VectorClock { - vc := &VectorClock{ - clock: make([]uint, len(a)), +func ArrayToVectorClock[T Uint](a []T) *VectorClock[T] { + vc := &VectorClock[T]{ + clock: make([]T, len(a)), } copy(vc.clock, a) return vc diff --git a/clock/clock_test.go b/clock/clock_test.go index d41f911..4fea26f 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -1,11 +1,12 @@ package clock import ( + "math" "reflect" "testing" ) -func assertClocksEqual(t testing.TB, got, want []uint) { +func assertClocksEqual[T Uint](t testing.TB, got, want []T) { t.Helper() if !reflect.DeepEqual(got, want) { @@ -16,24 +17,51 @@ func assertClocksEqual(t testing.TB, got, want []uint) { 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 { - a []uint - b []uint - expected []uint + a []uint32 + b []uint32 + expected []uint32 }{ - {[]uint{0, 0}, []uint{0, 0}, []uint{0, 0}}, - {[]uint{2, 0}, []uint{0, 2}, []uint{2, 2}}, - {[]uint{4, 11}, []uint{3, 10}, []uint{4, 11}}, - {[]uint{5, 9}, []uint{8, 12}, []uint{8, 12}}, - {[]uint{1, 1}, []uint{1, 1}, []uint{1, 1}}, - // {[]uint{math.MaxUint32, 1}, []uint{(math.MaxUint32 + 1), 1}, []uint{1, 1}}, + {[]uint32{0, 0}, []uint32{0, 0}, []uint32{0, 0}}, + {[]uint32{2, 0}, []uint32{0, 2}, []uint32{2, 2}}, + {[]uint32{4, 11}, []uint32{3, 10}, []uint32{4, 11}}, + {[]uint32{5, 9}, []uint32{8, 12}, []uint32{8, 12}}, + {[]uint32{1, 1}, []uint32{1, 1}, []uint32{1, 1}}, + {[]uint32{math.MaxUint32, 1}, []uint32{2, 1}, []uint32{4294967295, 1}}, } 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 { 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) { - vc := VectorClock{clock: []uint{4, 11}} + t.Run("test empty new", func(t *testing.T) { - 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 { - 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 { - t.Errorf("Sync should not have errored") - } + clock, err := vc.Increment(0) - 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) { - got := NewVectorClock(2) - want := VectorClock{clock: []uint{0, 0}} + t.Run("test new vc for uint32", func(t *testing.T) { + + 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) { t.Run("from empty slice", func(t *testing.T) { - got := ArrayToVectorClock([]uint{}) - want := VectorClock{ - clock: []uint{}, + got := ArrayToVectorClock([]uint32{}) + want := VectorClock[uint32]{ + clock: []uint32{}, } if len(got.GetClock()) != 0 { @@ -101,34 +167,34 @@ func TestArrayToVectorClock(t *testing.T) { }) t.Run("from zero slice", func(t *testing.T) { - got := ArrayToVectorClock(make([]uint, 6)) - want := VectorClock{ - clock: []uint{0, 0, 0, 0, 0, 0}, + got := ArrayToVectorClock(make([]uint64, 6)) + want := VectorClock[uint64]{ + clock: []uint64{0, 0, 0, 0, 0, 0}, } assertClocksEqual(t, got.GetClock(), want.GetClock()) }) t.Run("from start clock", func(t *testing.T) { - want := NewVectorClock(3) + want := NewVectorClock[uint64](3) got := ArrayToVectorClock(want.GetClock()) assertClocksEqual(t, got.GetClock(), want.GetClock()) }) t.Run("from a populated clock", func(t *testing.T) { - want := VectorClock{ - clock: []uint{2, 3}, + want := VectorClock[uint32]{ + clock: []uint32{2, 3}, } got := ArrayToVectorClock(want.GetClock()) assertClocksEqual(t, got.GetClock(), want.GetClock()) }) 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) - want := make([]uint, len(initial_a)) + want := make([]uint64, len(initial_a)) copy(want, initial_a) initial_a[0] = 12