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  }