|
| 1 | +// Copyright 2026-Present Couchbase, Inc. |
| 2 | +// |
| 3 | +// Use of this software is governed by the Business Source License included |
| 4 | +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified |
| 5 | +// in that file, in accordance with the Business Source License, use of this |
| 6 | +// software will be governed by the Apache License, Version 2.0, included in |
| 7 | +// the file licenses/APL2.txt. |
| 8 | + |
| 9 | +package base |
| 10 | + |
| 11 | +import ( |
| 12 | + "sync" |
| 13 | + "testing" |
| 14 | + "time" |
| 15 | + |
| 16 | + "github.com/stretchr/testify/assert" |
| 17 | + "github.com/stretchr/testify/require" |
| 18 | +) |
| 19 | + |
| 20 | +// hourNanos is an offset used to place test floors a wall-clock hour either side of "now". |
| 21 | +const hourNanos = uint64(time.Hour) |
| 22 | + |
| 23 | +// TestHybridLogicalClockMonotonic asserts successive values from the system-backed clock strictly increase. |
| 24 | +func TestHybridLogicalClockMonotonic(t *testing.T) { |
| 25 | + hlc := NewHybridLogicalClock() |
| 26 | + prev := hlc.Now(0) |
| 27 | + for i := 0; i < 1000; i++ { |
| 28 | + next := hlc.Now(0) |
| 29 | + require.Greater(t, next, prev, "value at iteration %d did not strictly increase", i) |
| 30 | + prev = next |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +// TestHybridLogicalClockTracksWallClock asserts Now(0) reflects the wall clock (physical component) with |
| 35 | +// its logical bits cleared, rather than drifting off into logical-counter space from a cold start. |
| 36 | +func TestHybridLogicalClockTracksWallClock(t *testing.T) { |
| 37 | + hlc := NewHybridLogicalClock() |
| 38 | + before := uint64(time.Now().UnixNano()) &^ hlcLogicalMask |
| 39 | + got := hlc.Now(0) |
| 40 | + after := uint64(time.Now().UnixNano()) |
| 41 | + require.GreaterOrEqual(t, got, before) |
| 42 | + require.LessOrEqual(t, got, after) |
| 43 | +} |
| 44 | + |
| 45 | +// TestHybridLogicalClockSameInstant asserts that when the wall clock does not advance, successive values |
| 46 | +// still strictly increase via the logical counter. |
| 47 | +func TestHybridLogicalClockSameInstant(t *testing.T) { |
| 48 | + now := uint64(time.Now().UnixNano()) |
| 49 | + hlc := &HybridLogicalClock{clock: func() uint64 { return now }} |
| 50 | + |
| 51 | + first := hlc.Now(0) |
| 52 | + require.Equal(t, now&^hlcLogicalMask, first) |
| 53 | + require.Equal(t, first+1, hlc.Now(0)) |
| 54 | + require.Equal(t, first+2, hlc.Now(0)) |
| 55 | +} |
| 56 | + |
| 57 | +// TestHybridLogicalClockPhysicalMask asserts the logical bits of the wall clock are cleared so generated |
| 58 | +// values stay in the CAS numeric space. |
| 59 | +func TestHybridLogicalClockPhysicalMask(t *testing.T) { |
| 60 | + // OR in low bits so masking is observable regardless of the current nanosecond. |
| 61 | + now := uint64(time.Now().UnixNano()) | hlcLogicalMask |
| 62 | + hlc := &HybridLogicalClock{clock: func() uint64 { return now }} |
| 63 | + |
| 64 | + got := hlc.Now(0) |
| 65 | + // Isolates only bits 15–0 of the result. Requires them to be all zero — proving Now() stripped them via &^ hlcLogicalMask. |
| 66 | + require.Zero(t, got&hlcLogicalMask, "low %d (logical) bits should be cleared", hlcLogicalBits) |
| 67 | + // Both sides clear the low 16 bits of now |
| 68 | + require.Equal(t, now&^hlcLogicalMask, got) |
| 69 | +} |
| 70 | + |
| 71 | +// TestHybridLogicalClockFloor asserts Now never returns a value <= floor. |
| 72 | +func TestHybridLogicalClockFloor(t *testing.T) { |
| 73 | + t.Run("floor below physical is ignored", func(t *testing.T) { |
| 74 | + now := uint64(time.Now().UnixNano()) |
| 75 | + hlc := &HybridLogicalClock{clock: func() uint64 { return now }} |
| 76 | + got := hlc.Now(now - hourNanos) // a floor an hour in the past |
| 77 | + require.Equal(t, now&^hlcLogicalMask, got) |
| 78 | + }) |
| 79 | + |
| 80 | + t.Run("floor above physical wins", func(t *testing.T) { |
| 81 | + now := uint64(time.Now().UnixNano()) |
| 82 | + hlc := &HybridLogicalClock{clock: func() uint64 { return now }} |
| 83 | + floor := now + hourNanos // a floor an hour in the future |
| 84 | + got := hlc.Now(floor) |
| 85 | + require.Equal(t, floor+1, got) |
| 86 | + require.Greater(t, got, floor) |
| 87 | + }) |
| 88 | +} |
| 89 | + |
| 90 | +// TestHybridLogicalClockNoFloor covers the brand-new-document / absent-source case where floor is 0: Now |
| 91 | +// behaves as an ordinary tick rather than special-casing. |
| 92 | +func TestHybridLogicalClockNoFloor(t *testing.T) { |
| 93 | + now := uint64(time.Now().UnixNano()) |
| 94 | + hlc := &HybridLogicalClock{clock: func() uint64 { return now }} |
| 95 | + require.Equal(t, now&^hlcLogicalMask, hlc.Now(0)) |
| 96 | +} |
| 97 | + |
| 98 | +// TestHybridLogicalClockConcurrent asserts thread-safety: concurrent callers never receive duplicate |
| 99 | +// values (strict monotonicity implies uniqueness). |
| 100 | +func TestHybridLogicalClockConcurrent(t *testing.T) { |
| 101 | + hlc := NewHybridLogicalClock() |
| 102 | + |
| 103 | + const goroutines = 16 |
| 104 | + const perGoroutine = 500 |
| 105 | + |
| 106 | + var wg sync.WaitGroup |
| 107 | + results := make([][]uint64, goroutines) |
| 108 | + for g := 0; g < goroutines; g++ { |
| 109 | + wg.Add(1) |
| 110 | + go func(g int) { |
| 111 | + defer wg.Done() |
| 112 | + values := make([]uint64, perGoroutine) |
| 113 | + for i := range values { |
| 114 | + values[i] = hlc.Now(0) |
| 115 | + } |
| 116 | + results[g] = values |
| 117 | + }(g) |
| 118 | + } |
| 119 | + wg.Wait() |
| 120 | + |
| 121 | + seen := make(map[uint64]struct{}, goroutines*perGoroutine) |
| 122 | + for _, values := range results { |
| 123 | + for _, v := range values { |
| 124 | + _, dup := seen[v] |
| 125 | + assert.False(t, dup, "duplicate value %d returned to concurrent callers", v) |
| 126 | + seen[v] = struct{}{} |
| 127 | + } |
| 128 | + } |
| 129 | + require.Len(t, seen, goroutines*perGoroutine) |
| 130 | +} |
0 commit comments