github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/revisions/hlcrevision.go (about) 1 package revisions 2 3 import ( 4 "fmt" 5 "math" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/shopspring/decimal" 11 12 "github.com/authzed/spicedb/pkg/datastore" 13 "github.com/authzed/spicedb/pkg/spiceerrors" 14 ) 15 16 var zeroHLC = HLCRevision{} 17 18 // NOTE: This *must* match the length defined in CRDB or the implementation below will break. 19 const logicalClockLength = 10 20 21 var logicalClockOffset = uint32(math.Pow10(logicalClockLength + 1)) 22 23 // HLCRevision is a revision that is a hybrid logical clock, stored as two integers. 24 // The first integer is the timestamp in nanoseconds, and the second integer is the 25 // logical clock defined as 11 digits, with the first digit being ignored to ensure 26 // precision of the given logical clock. 27 type HLCRevision struct { 28 time int64 29 logicalclock uint32 30 } 31 32 // parseHLCRevisionString parses a string into a hybrid logical clock revision. 33 func parseHLCRevisionString(revisionStr string) (datastore.Revision, error) { 34 pieces := strings.Split(revisionStr, ".") 35 if len(pieces) == 1 { 36 // If there is no decimal point, assume the revision is a timestamp. 37 timestamp, err := strconv.ParseInt(pieces[0], 10, 64) 38 if err != nil { 39 return datastore.NoRevision, fmt.Errorf("invalid revision string: %q", revisionStr) 40 } 41 return HLCRevision{timestamp, 0}, nil 42 } 43 44 if len(pieces) != 2 { 45 return datastore.NoRevision, fmt.Errorf("invalid revision string: %q", revisionStr) 46 } 47 48 timestamp, err := strconv.ParseInt(pieces[0], 10, 64) 49 if err != nil { 50 return datastore.NoRevision, fmt.Errorf("invalid revision string: %q", revisionStr) 51 } 52 53 if len(pieces[1]) > logicalClockLength { 54 return datastore.NoRevision, spiceerrors.MustBugf("invalid revision string due to unexpected logical clock size (%d): %q", len(pieces[1]), revisionStr) 55 } 56 57 paddedLogicalClockStr := pieces[1] + strings.Repeat("0", logicalClockLength-len(pieces[1])) 58 logicalclock, err := strconv.ParseInt(paddedLogicalClockStr, 10, 32) 59 if err != nil { 60 return datastore.NoRevision, fmt.Errorf("invalid revision string: %q", revisionStr) 61 } 62 63 return HLCRevision{timestamp, uint32(logicalclock) + logicalClockOffset}, nil 64 } 65 66 // HLCRevisionFromString parses a string into a hybrid logical clock revision. 67 func HLCRevisionFromString(revisionStr string) (HLCRevision, error) { 68 rev, err := parseHLCRevisionString(revisionStr) 69 if err != nil { 70 return zeroHLC, err 71 } 72 73 return rev.(HLCRevision), nil 74 } 75 76 // NewForHLC creates a new revision for the given hybrid logical clock. 77 func NewForHLC(decimal decimal.Decimal) (HLCRevision, error) { 78 rev, err := HLCRevisionFromString(decimal.String()) 79 if err != nil { 80 return zeroHLC, fmt.Errorf("invalid HLC decimal: %v (%s) => %w", decimal, decimal.String(), err) 81 } 82 83 return rev, nil 84 } 85 86 // NewHLCForTime creates a new revision for the given time. 87 func NewHLCForTime(time time.Time) HLCRevision { 88 return HLCRevision{time.UnixNano(), 0} 89 } 90 91 func (hlc HLCRevision) Equal(rhs datastore.Revision) bool { 92 if rhs == datastore.NoRevision { 93 rhs = zeroHLC 94 } 95 96 rhsHLC := rhs.(HLCRevision) 97 return hlc.time == rhsHLC.time && hlc.logicalclock == rhsHLC.logicalclock 98 } 99 100 func (hlc HLCRevision) GreaterThan(rhs datastore.Revision) bool { 101 if rhs == datastore.NoRevision { 102 rhs = zeroHLC 103 } 104 105 rhsHLC := rhs.(HLCRevision) 106 return hlc.time > rhsHLC.time || (hlc.time == rhsHLC.time && hlc.logicalclock > rhsHLC.logicalclock) 107 } 108 109 func (hlc HLCRevision) LessThan(rhs datastore.Revision) bool { 110 if rhs == datastore.NoRevision { 111 rhs = zeroHLC 112 } 113 114 rhsHLC := rhs.(HLCRevision) 115 return hlc.time < rhsHLC.time || (hlc.time == rhsHLC.time && hlc.logicalclock < rhsHLC.logicalclock) 116 } 117 118 func (hlc HLCRevision) String() string { 119 if hlc.logicalclock == 0 { 120 return strconv.FormatInt(hlc.time, 10) 121 } 122 123 logicalClockString := strconv.FormatInt(int64(hlc.logicalclock)-int64(logicalClockOffset), 10) 124 return strconv.FormatInt(hlc.time, 10) + "." + strings.Repeat("0", logicalClockLength-len(logicalClockString)) + logicalClockString 125 } 126 127 func (hlc HLCRevision) TimestampNanoSec() int64 { 128 return hlc.time 129 } 130 131 func (hlc HLCRevision) InexactFloat64() float64 { 132 if hlc.logicalclock == 0 { 133 return float64(hlc.time) 134 } 135 136 return float64(hlc.time) + float64(hlc.logicalclock-logicalClockOffset)/math.Pow10(logicalClockLength) 137 } 138 139 func (hlc HLCRevision) ConstructForTimestamp(timestamp int64) WithTimestampRevision { 140 return HLCRevision{timestamp, 0} 141 } 142 143 func (hlc HLCRevision) IntegerRepresentation() (int64, uint32) { 144 return hlc.time, hlc.logicalclock 145 } 146 147 var ( 148 _ datastore.Revision = HLCRevision{} 149 _ WithTimestampRevision = HLCRevision{} 150 ) 151 152 // HLCKeyFunc is used to convert a simple HLC for use in maps. 153 func HLCKeyFunc(r HLCRevision) HLCRevision { 154 return r 155 } 156 157 // HLCKeyLessThanFunc is used to compare keys created by the HLCKeyFunc. 158 func HLCKeyLessThanFunc(lhs, rhs HLCRevision) bool { 159 return lhs.time < rhs.time || (lhs.time == rhs.time && lhs.logicalclock < rhs.logicalclock) 160 }