Skip to content

Commit

Permalink
Storage Engine
Browse files Browse the repository at this point in the history
  • Loading branch information
bbengfort committed Feb 22, 2025
1 parent 7e3f9ee commit 5ef8708
Show file tree
Hide file tree
Showing 17 changed files with 335 additions and 15 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
module github.com/rotationalio/honu

go 1.23.1
go 1.23.3

require (
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/google/go-querystring v1.1.0
github.com/joho/godotenv v1.5.1
github.com/julienschmidt/httprouter v1.3.0
github.com/oklog/ulid/v2 v2.1.0
github.com/rotationalio/confire v1.1.0
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.9.0
github.com/tinylib/msgp v1.2.4
github.com/urfave/cli/v2 v2.27.5
go.rtnl.ai/ulid v1.1.1
)

require (
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand All @@ -42,6 +39,8 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.rtnl.ai/ulid v1.1.1 h1:7zDInpWEeiKFcDoJyonails5lQxwOr4wc8K4TAuOVNE=
go.rtnl.ai/ulid v1.1.1/go.mod h1:F95yPYwEEZdz5sM4GC6buziff6xD+7XVcGZ/8n37Cn0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/v1/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import (

"github.com/cenkalti/backoff"
"github.com/google/go-querystring/query"
"github.com/oklog/ulid/v2"
"github.com/rotationalio/honu/pkg/api/v1/credentials"
"github.com/rs/zerolog/log"
"go.rtnl.ai/ulid"
)

const (
Expand Down
93 changes: 93 additions & 0 deletions pkg/store/key/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package key

import (
"bytes"
"encoding/binary"
"errors"

"github.com/rotationalio/honu/pkg/store/lamport"
"go.rtnl.ai/ulid"
)

const (
// The default size of a v1 object storage key.
keySize int = 45

// The version of the key for compatibility indication; increment this number any time
// the underlying key data is no longer compatible with the previous version.
keyVersion byte = 0x1
)

var (
ErrBadVersion = errors.New("key is malformed: cannot decode specified version")
ErrBadSize = errors.New("key is malformed: incorrect size")
ErrMalformed = errors.New("key is malformed: cannot parse version components")
)

// Keys are used to store objects in the underlying key/value store. It is a 45 byte key
// that is composed of 16 byte object and collection IDs and a 4 byte uint32 and 8 byte
// uint64 representing the lamport scalar version number. The last byte indicates the
// key version and marshaling compatibility. There are no separator characters
// between the components of the key since all components are a fixed length.
//
// A key is structured as: collection::oid::vid::pid::keyVersion
//
// Note that the version is serialized differently than the lamport scalar in order to
// maintain lexicographic sorting of the the data.
type Key []byte

func New(oid, cid ulid.ULID, vers lamport.Scalar) Key {
key := make([]byte, keySize)
copy(key[0:16], oid[:])
copy(key[16:32], cid[:])
binary.BigEndian.PutUint64(key[32:40], vers.VID)
binary.BigEndian.PutUint32(key[40:44], vers.PID)
key[44] = keyVersion
return Key(key)
}

func (k Key) ObjectID() ulid.ULID {
if err := k.Check(); err != nil {
panic(err)
}
return ulid.ULID(k[0:16])
}

func (k Key) CollectionID() ulid.ULID {
if err := k.Check(); err != nil {
panic(err)
}
return ulid.ULID(k[16:32])
}

func (k Key) Version() lamport.Scalar {
if err := k.Check(); err != nil {
panic(err)
}
return lamport.Scalar{
VID: binary.BigEndian.Uint64(k[32:40]),
PID: binary.BigEndian.Uint32(k[40:44]),
}
}

func (k Key) Check() error {
if len(k) != keySize {
return ErrBadSize
}

if k[44] != keyVersion {
return ErrBadVersion
}

return nil
}

//===========================================================================
// Sort Interface
//===========================================================================

type Keys []Key

func (k Keys) Len() int { return len(k) }
func (k Keys) Less(i, j int) bool { return bytes.Compare(k[i][:], k[j][:]) < 0 }
func (k Keys) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
153 changes: 153 additions & 0 deletions pkg/store/key/key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package key_test

import (
"bytes"
crand "crypto/rand"
"math/rand/v2"
"sort"
"testing"

"github.com/rotationalio/honu/pkg/store/key"
"github.com/rotationalio/honu/pkg/store/lamport"
"github.com/stretchr/testify/require"
"go.rtnl.ai/ulid"
)

func TestNewKey(t *testing.T) {
oid := ulid.Make()
cid := ulid.Make()
vers := lamport.Scalar{VID: 1, PID: 2}

k := key.New(oid, cid, vers)
require.NotNil(t, k)
require.Equal(t, 45, len(k))
require.Equal(t, oid, k.ObjectID())
require.Equal(t, cid, k.CollectionID())
require.Equal(t, vers, k.Version())
}

func TestKeyLexicographic(t *testing.T) {
// For the same collection and object ID the keys should be lexicographically sorted
// by the version to ensure that we can read the latest version either by choosing
// the first item in a list of sorted keys or the last item.
keys := make(key.Keys, 512)
oid := ulid.Make()
cid := ulid.Make()
vers := &lamport.Scalar{VID: 1, PID: 1}

// Create a list of keys with monotonically increasing versions.
for i := 0; i < len(keys); i++ {
keys[i] = key.New(oid, cid, *vers)
vers = randNextScalar(vers)
}

// Ensure the keys are sorted both by monotonically increasing version and by
// lexicographic byte order.
for i := 1; i < len(keys); i++ {
versa, versb := keys[i-1].Version(), keys[i].Version()
require.True(t, versa.Before(&versb) || versa.Equals(&versb), "keys[%d] version is not before keys[%d] version", i-1, i)
require.True(t, bytes.Compare(keys[i-1][:], keys[i][:]) <= 0, "keys[%d] is not less than or equal to keys[%d]", i-1, i)
}
}

func TestKeyCheck(t *testing.T) {
oid := ulid.Make()
cid := ulid.Make()
vers := lamport.Scalar{VID: 1, PID: 2}

t.Run("Valid", func(t *testing.T) {
k := key.New(oid, cid, vers)
require.NoError(t, k.Check())
})

t.Run("BadSize", func(t *testing.T) {
badKey := key.Key(make([]byte, 42))
require.ErrorIs(t, badKey.Check(), key.ErrBadSize)
})

t.Run("BadVersion", func(t *testing.T) {
badKey := key.New(oid, cid, vers)
badKey[44] = 0x2
require.ErrorIs(t, badKey.Check(), key.ErrBadVersion)
})
}

func TestObjectID(t *testing.T) {
oid := ulid.Make()
cid := ulid.Make()
vers := lamport.Scalar{VID: 80, PID: 122}

t.Run("Ok", func(t *testing.T) {
k := key.New(oid, cid, vers)
require.Equal(t, oid, k.ObjectID())
})

t.Run("Panics", func(t *testing.T) {
badKey := key.Key(make([]byte, 42))
require.Panics(t, func() {
badKey.ObjectID()
})
})
}

func TestCollectionID(t *testing.T) {
oid := ulid.Make()
cid := ulid.Make()
vers := lamport.Scalar{VID: 391, PID: 8}

t.Run("Ok", func(t *testing.T) {
k := key.New(oid, cid, vers)
require.Equal(t, cid, k.CollectionID())
})

t.Run("Panics", func(t *testing.T) {
badKey := key.Key(make([]byte, 42))
require.Panics(t, func() {
badKey.CollectionID()
})
})
}

func TestVersion(t *testing.T) {
oid := ulid.Make()
cid := ulid.Make()
vers := lamport.Scalar{VID: 5, PID: 1}

t.Run("Ok", func(t *testing.T) {
k := key.New(oid, cid, vers)
require.Equal(t, vers, k.Version())
})

t.Run("Panics", func(t *testing.T) {
badKey := key.Key(make([]byte, 42))
require.Panics(t, func() {
badKey.Version()
})
})
}

func TestSort(t *testing.T) {
keys := make(key.Keys, 128)
for i := 0; i < 128; i++ {
keys[i] = randKey()
}

sort.Sort(keys)

for i := 1; i < len(keys); i++ {
require.True(t, bytes.Compare(keys[i-1][:], keys[i][:]) <= 0, "keys[%d] is not less than or equal to keys[%d]", i-1, i)
}
}

func randKey() key.Key {
b := make([]byte, 45)
crand.Read(b)
return key.Key(b)
}

func randNextScalar(prev *lamport.Scalar) *lamport.Scalar {
s := &lamport.Scalar{}
s.PID = uint32(rand.Int32N(24))
s.VID = uint64(rand.Int64N(32)) + prev.VID
return s
}
10 changes: 10 additions & 0 deletions pkg/store/lamport/scalar.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,13 @@ func (s *Scalar) UnmarshalText(text []byte) (err error) {
func (s *Scalar) String() string {
return fmt.Sprintf("%d.%d", s.PID, s.VID)
}

//===========================================================================
// Sort Interface
//===========================================================================

type Scalars []*Scalar

func (s Scalars) Len() int { return len(s) }
func (s Scalars) Less(i, j int) bool { return s[i].Before(s[j]) }
func (s Scalars) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
25 changes: 25 additions & 0 deletions pkg/store/lamport/scalar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lamport_test
import (
"encoding/json"
"math/rand/v2"
"sort"
"testing"

. "github.com/rotationalio/honu/pkg/store/lamport"
Expand Down Expand Up @@ -168,6 +169,30 @@ func TestCompare(t *testing.T) {
}

}

}

func TestSort(t *testing.T) {
// Create a list of random scalars
scalars := make(Scalars, 128)
for i := range scalars {
scalars[i] = randScalar()
}

// Sort the scalars
sort.Sort(scalars)

// Ensure that the scalars are sorted
for i := 1; i < len(scalars); i++ {
require.True(t, scalars[i-1].Before(scalars[i]) || scalars[i-1].Equals(scalars[i]), "scalars[%d] is not before or equal to scalars[%d]", i-1, i)
}
}

func randScalar() *Scalar {
return &Scalar{
PID: uint32(rand.Int32N(24)),
VID: uint64(rand.Int64N(10000)),
}
}

func randNextScalar(prev *Scalar) *Scalar {
Expand Down
2 changes: 1 addition & 1 deletion pkg/store/lani/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"io"
"time"

"github.com/oklog/ulid/v2"
"go.rtnl.ai/ulid"
)

// Decoder is similar to a bytes.Reader, allowing a sequential decoding of byte frames,
Expand Down
2 changes: 1 addition & 1 deletion pkg/store/lani/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"testing"
"time"

"github.com/oklog/ulid/v2"
. "github.com/rotationalio/honu/pkg/store/lani"
"github.com/stretchr/testify/require"
"go.rtnl.ai/ulid"
)

func TestUnmarshal(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/store/lani/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"reflect"
"time"

"github.com/oklog/ulid/v2"
"go.rtnl.ai/ulid"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion pkg/store/lani/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"testing"
"time"

"github.com/oklog/ulid/v2"
. "github.com/rotationalio/honu/pkg/store/lani"
"github.com/stretchr/testify/require"
"go.rtnl.ai/ulid"
)

func TestMarshal(t *testing.T) {
Expand Down
Loading

0 comments on commit 5ef8708

Please sign in to comment.