This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"errors"
"github.com/cockroachdb/pebble"
"sync"
)
type DB struct {
store *Store
name string
namespace string
tableMap map[string]TableInterface
mu sync.RWMutex
}
func NewDB(store *Store, dbName string) (*DB, error) {
if !IsValidName(dbName) {
return nil, errors.New("invalid database name '" + dbName + "'")
}
return &DB{
store: store,
name: dbName,
namespace: "$" + dbName + "$",
tableMap: map[string]TableInterface{},
}, nil
}
func (this *DB) AddTable(table TableInterface) {
table.SetNamespace([]byte(this.Namespace() + table.Name() + "$"))
table.SetDB(this)
this.mu.Lock()
defer this.mu.Unlock()
this.tableMap[table.Name()] = table
}
func (this *DB) Name() string {
return this.name
}
func (this *DB) Namespace() string {
return this.namespace
}
func (this *DB) Store() *Store {
return this.store
}
func (this *DB) Inspect(fn func(key []byte, value []byte)) error {
it, err := this.store.rawDB.NewIter(&pebble.IterOptions{
LowerBound: []byte(this.namespace),
UpperBound: append([]byte(this.namespace), 0xFF, 0xFF),
})
if err != nil {
return err
}
defer func() {
_ = it.Close()
}()
for it.First(); it.Valid(); it.Next() {
value, valueErr := it.ValueAndErr()
if valueErr != nil {
return valueErr
}
fn(it.Key(), value)
}
return nil
}
// Truncate the database
func (this *DB) Truncate() error {
this.mu.Lock()
defer this.mu.Unlock()
var start = []byte(this.Namespace())
return this.store.rawDB.DeleteRange(start, append(start, 0xFF), DefaultWriteOptions)
}
func (this *DB) Close() error {
this.mu.Lock()
defer this.mu.Unlock()
var lastErr error
for _, table := range this.tableMap {
err := table.Close()
if err != nil {
lastErr = err
}
}
return lastErr
}

View File

@@ -0,0 +1,55 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/cockroachdb/pebble"
"testing"
)
func TestNewDB(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
_, err = store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
testingStore = store
testInspectDB(t)
}
func testInspectDB(t *testing.T) {
if testingStore == nil {
return
}
it, err := testingStore.RawDB().NewIter(&pebble.IterOptions{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = it.Close()
}()
var isSingleTesting = testutils.IsSingleTesting()
for it.First(); it.Valid(); it.Next() {
valueBytes, valueErr := it.ValueAndErr()
if valueErr != nil {
t.Fatal(valueErr)
}
t.Log(string(it.Key()), "=>", string(valueBytes))
if !isSingleTesting {
break
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"errors"
"fmt"
"github.com/cockroachdb/pebble"
)
var ErrTableNotFound = errors.New("table not found")
var ErrKeyTooLong = errors.New("too long key")
var ErrSkip = errors.New("skip") // skip count in iterator
var ErrTableClosed = errors.New("table closed")
func IsNotFound(err error) bool {
return err != nil && errors.Is(err, pebble.ErrNotFound)
}
func IsSkipError(err error) bool {
return err != nil && errors.Is(err, ErrSkip)
}
func Skip() (bool, error) {
return true, ErrSkip
}
func NewTableClosedErr(tableName string) error {
return fmt.Errorf("table '"+tableName+"' closed: %w", ErrTableClosed)
}

View File

@@ -0,0 +1,9 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type Item[T any] struct {
Key string
Value T
FieldKey []byte
}

View File

@@ -0,0 +1,17 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import "github.com/cockroachdb/pebble"
type IteratorOptions struct {
LowerBound []byte
UpperBound []byte
}
func (this *IteratorOptions) RawOptions() *pebble.IterOptions {
return &pebble.IterOptions{
LowerBound: this.LowerBound,
UpperBound: this.UpperBound,
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
)
type Logger struct {
}
func NewLogger() *Logger {
return &Logger{}
}
func (this *Logger) Infof(format string, args ...any) {
// stub
}
func (this *Logger) Errorf(format string, args ...any) {
remotelogs.Error("KV", fmt.Sprintf(format, args...))
}
func (this *Logger) Fatalf(format string, args ...any) {
remotelogs.Error("KV", fmt.Sprintf(format, args...))
}

View File

@@ -0,0 +1,13 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import "github.com/cockroachdb/pebble"
var DefaultWriteOptions = &pebble.WriteOptions{
Sync: false,
}
var DefaultWriteSyncOptions = &pebble.WriteOptions{
Sync: true,
}

View File

@@ -0,0 +1,496 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"bytes"
"errors"
"fmt"
byteutils "github.com/TeaOSLab/EdgeNode/internal/utils/byte"
)
type DataType = int
const (
DataTypeKey DataType = 1
DataTypeField DataType = 2
)
type QueryOperator int
const (
QueryOperatorGt QueryOperator = 1
QueryOperatorGte QueryOperator = 2
QueryOperatorLt QueryOperator = 3
QueryOperatorLte QueryOperator = 4
)
type QueryOperatorInfo struct {
Operator QueryOperator
Value any
}
type IteratorFunc[T any] func(tx *Tx[T], item Item[T]) (goNext bool, err error)
type Query[T any] struct {
table *Table[T]
tx *Tx[T]
dataType int
offsetKey string
limit int
prefix string
reverse bool
forUpdate bool
keysOnly bool
fieldName string
fieldReverse bool
fieldOperators []QueryOperatorInfo
fieldPrefix string
fieldOffsetKey []byte
}
func NewQuery[T any]() *Query[T] {
return &Query[T]{
limit: -1,
dataType: DataTypeKey,
}
}
func (this *Query[T]) SetTable(table *Table[T]) *Query[T] {
this.table = table
return this
}
func (this *Query[T]) SetTx(tx *Tx[T]) *Query[T] {
this.tx = tx
return this
}
func (this *Query[T]) ForKey() *Query[T] {
this.dataType = DataTypeKey
return this
}
func (this *Query[T]) ForField() *Query[T] {
this.dataType = DataTypeField
return this
}
func (this *Query[T]) Limit(limit int) *Query[T] {
this.limit = limit
return this
}
func (this *Query[T]) Offset(offsetKey string) *Query[T] {
this.offsetKey = offsetKey
return this
}
func (this *Query[T]) Prefix(prefix string) *Query[T] {
this.prefix = prefix
return this
}
func (this *Query[T]) Desc() *Query[T] {
this.reverse = true
return this
}
func (this *Query[T]) ForUpdate() *Query[T] {
this.forUpdate = true
return this
}
func (this *Query[T]) KeysOnly() *Query[T] {
this.keysOnly = true
return this
}
func (this *Query[T]) FieldAsc(fieldName string) *Query[T] {
this.fieldName = fieldName
this.fieldReverse = false
return this
}
func (this *Query[T]) FieldDesc(fieldName string) *Query[T] {
this.fieldName = fieldName
this.fieldReverse = true
return this
}
func (this *Query[T]) FieldPrefix(fieldName string, fieldPrefix string) *Query[T] {
this.fieldName = fieldName
this.fieldPrefix = fieldPrefix
return this
}
func (this *Query[T]) FieldOffset(fieldOffset []byte) *Query[T] {
this.fieldOffsetKey = fieldOffset
return this
}
//func (this *Query[T]) FieldLt(value any) *Query[T] {
// this.fieldOperators = append(this.fieldOperators, QueryOperatorInfo{
// Operator: QueryOperatorLt,
// Value: value,
// })
// return this
//}
//
//func (this *Query[T]) FieldLte(value any) *Query[T] {
// this.fieldOperators = append(this.fieldOperators, QueryOperatorInfo{
// Operator: QueryOperatorLte,
// Value: value,
// })
// return this
//}
//
//func (this *Query[T]) FieldGt(value any) *Query[T] {
// this.fieldOperators = append(this.fieldOperators, QueryOperatorInfo{
// Operator: QueryOperatorGt,
// Value: value,
// })
// return this
//}
//
//func (this *Query[T]) FieldGte(value any) *Query[T] {
// this.fieldOperators = append(this.fieldOperators, QueryOperatorInfo{
// Operator: QueryOperatorGte,
// Value: value,
// })
// return this
//}
func (this *Query[T]) FindAll(fn IteratorFunc[T]) (err error) {
if this.table != nil && this.table.isClosed {
return NewTableClosedErr(this.table.name)
}
defer func() {
var panicErr = recover()
if panicErr != nil {
resultErr, ok := panicErr.(error)
if ok {
err = fmt.Errorf("execute query failed: %w", resultErr)
}
}
}()
if this.tx != nil {
defer func() {
_ = this.tx.Close()
}()
var itErr error
if len(this.fieldName) == 0 {
itErr = this.iterateKeys(fn)
} else {
itErr = this.iterateFields(fn)
}
if itErr != nil {
return itErr
}
return this.tx.Commit()
}
if this.table != nil {
var txFn func(fn func(tx *Tx[T]) error) error
if this.forUpdate {
txFn = this.table.WriteTx
} else {
txFn = this.table.ReadTx
}
return txFn(func(tx *Tx[T]) error {
this.tx = tx
if len(this.fieldName) == 0 {
return this.iterateKeys(fn)
}
return this.iterateFields(fn)
})
}
return errors.New("current query require 'table' or 'tx'")
}
func (this *Query[T]) iterateKeys(fn IteratorFunc[T]) error {
if this.limit == 0 {
return nil
}
var opt = &IteratorOptions{}
var prefix []byte
switch this.dataType {
case DataTypeKey:
prefix = byteutils.Append(this.table.Namespace(), []byte(KeyPrefix)...)
case DataTypeField:
prefix = byteutils.Append(this.table.Namespace(), []byte(FieldPrefix)...)
default:
prefix = byteutils.Append(this.table.Namespace(), []byte(KeyPrefix)...)
}
var prefixLen = len(prefix)
if len(this.prefix) > 0 {
prefix = append(prefix, this.prefix...)
}
var offsetKey []byte
if this.reverse {
if len(this.offsetKey) > 0 {
offsetKey = byteutils.Append(prefix, []byte(this.offsetKey)...)
} else {
offsetKey = byteutils.Append(prefix, 0xFF)
}
opt.LowerBound = prefix
opt.UpperBound = offsetKey
} else {
if len(this.offsetKey) > 0 {
offsetKey = byteutils.Append(prefix, []byte(this.offsetKey)...)
} else {
offsetKey = prefix
}
opt.LowerBound = offsetKey
opt.UpperBound = byteutils.Append(prefix, 0xFF)
}
var hasOffsetKey = len(this.offsetKey) > 0
it, itErr := this.tx.NewIterator(opt)
if itErr != nil {
return itErr
}
defer func() {
_ = it.Close()
}()
var count int
var itemFn = func() (goNextItem bool, err error) {
var keyBytes = it.Key()
// skip first offset key
if hasOffsetKey {
hasOffsetKey = false
if bytes.Equal(keyBytes, offsetKey) {
return true, nil
}
}
// call fn
var value T
if !this.keysOnly {
valueBytes, valueErr := it.ValueAndErr()
if valueErr != nil {
return false, valueErr
}
value, err = this.table.encoder.Decode(valueBytes)
if err != nil {
return false, err
}
}
goNext, callbackErr := fn(this.tx, Item[T]{
Key: string(keyBytes[prefixLen:]),
Value: value,
})
if callbackErr != nil {
if IsSkipError(callbackErr) {
return true, nil
} else {
return false, callbackErr
}
}
if !goNext {
return false, nil
}
// limit
if this.limit > 0 {
count++
if count >= this.limit {
return false, nil
}
}
return true, nil
}
if this.reverse {
for it.Last(); it.Valid(); it.Prev() {
goNext, itemErr := itemFn()
if itemErr != nil {
return itemErr
}
if !goNext {
break
}
}
} else {
for it.First(); it.Valid(); it.Next() {
goNext, itemErr := itemFn()
if itemErr != nil {
return itemErr
}
if !goNext {
break
}
}
}
return nil
}
func (this *Query[T]) iterateFields(fn IteratorFunc[T]) error {
if this.limit == 0 {
return nil
}
var hasOffsetKey = len(this.offsetKey) > 0 || len(this.fieldOffsetKey) > 0
var opt = &IteratorOptions{}
var prefix = this.table.FieldKey(this.fieldName)
prefix = append(prefix, '$')
if len(this.fieldPrefix) > 0 {
prefix = append(prefix, this.fieldPrefix...)
}
var offsetKey []byte
if this.fieldReverse {
if len(this.fieldOffsetKey) > 0 {
offsetKey = this.fieldOffsetKey
} else if len(this.offsetKey) > 0 {
offsetKey = byteutils.Append(prefix, []byte(this.offsetKey)...)
} else {
offsetKey = byteutils.Append(prefix, 0xFF)
}
opt.LowerBound = prefix
opt.UpperBound = offsetKey
} else {
if len(this.fieldOffsetKey) > 0 {
offsetKey = this.fieldOffsetKey
} else if len(this.offsetKey) > 0 {
offsetKey = byteutils.Append(prefix, []byte(this.offsetKey)...)
offsetKey = append(offsetKey, 0)
} else {
offsetKey = prefix
}
opt.LowerBound = offsetKey
opt.UpperBound = byteutils.Append(prefix, 0xFF)
}
it, itErr := this.tx.NewIterator(opt)
if itErr != nil {
return itErr
}
defer func() {
_ = it.Close()
}()
var count int
var itemFn = func() (goNextItem bool, err error) {
var fieldKeyBytes = it.Key()
fieldValueBytes, keyBytes, decodeKeyErr := this.table.DecodeFieldKey(this.fieldName, fieldKeyBytes)
if decodeKeyErr != nil {
return false, decodeKeyErr
}
// skip first offset key
if hasOffsetKey {
hasOffsetKey = false
if (len(this.fieldOffsetKey) > 0 && bytes.Equal(fieldKeyBytes, this.fieldOffsetKey)) ||
bytes.Equal(fieldValueBytes, []byte(this.offsetKey)) {
return true, nil
}
}
// 执行operators
if len(this.fieldOperators) > 0 {
if !this.matchOperators(fieldValueBytes) {
return true, nil
}
}
var resultItem = Item[T]{
Key: string(keyBytes),
FieldKey: fieldKeyBytes,
}
if !this.keysOnly {
value, getErr := this.table.getWithKeyBytes(this.tx, this.table.FullKeyBytes(keyBytes))
if getErr != nil {
if IsNotFound(getErr) {
return true, nil
}
return false, getErr
}
resultItem.Value = value
}
goNextItem, err = fn(this.tx, resultItem)
if err != nil {
if IsSkipError(err) {
return true, nil
} else {
return false, err
}
}
if !goNextItem {
return false, nil
}
// limit
if this.limit > 0 {
count++
if count >= this.limit {
return false, nil
}
}
return true, nil
}
if this.reverse {
for it.Last(); it.Valid(); it.Prev() {
goNext, itemErr := itemFn()
if itemErr != nil {
return itemErr
}
if !goNext {
break
}
}
} else {
for it.First(); it.Valid(); it.Next() {
goNext, itemErr := itemFn()
if itemErr != nil {
return itemErr
}
if !goNext {
break
}
}
}
return nil
}
func (this *Query[T]) matchOperators(fieldValueBytes []byte) bool {
// TODO
return true
}

View File

@@ -0,0 +1,334 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"runtime"
"testing"
"time"
)
func TestQuery_FindAll(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
defer func() {
t.Log("cost:", time.Since(before).Seconds()*1000, "ms")
}()
err := table.
Query().
Limit(10).
//Offset("a1000").
//Desc().
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log("key:", item.Key, "value:", item.Value)
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestQuery_FindAll_Break(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
defer func() {
t.Log("cost:", time.Since(before).Seconds()*1000, "ms")
}()
var count int
err := table.
Query().
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log("key:", item.Key, "value:", item.Value)
count++
if count > 2 {
// break test
_ = table.DB().Store().Close()
}
return count < 3, nil
})
if err != nil {
t.Log(err)
}
}
func TestQuery_FindAll_Break_Closed(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
defer func() {
t.Log("cost:", time.Since(before).Seconds()*1000, "ms")
}()
var count int
err := table.
Query().
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log("key:", item.Key, "value:", item.Value)
count++
if count > 2 {
// break test
_ = table.DB().Store().Close()
}
return count < 3, nil
})
t.Log("expected error:", err)
}
func TestQuery_FindAll_Desc(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
err := table.Query().
Desc().
Limit(10).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log("key:", item.Key, "value:", item.Value)
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestQuery_FindAll_Offset(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
{
t.Log("=== forward ===")
err := table.Query().
Offset("a3").
Limit(10).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log("key:", item.Key, "value:", item.Value)
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
{
t.Log("=== backward ===")
err := table.Query().
Desc().
Offset("a3").
Limit(10).
//KeyOnly().
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log("key:", item.Key, "value:", item.Value)
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
}
func TestQuery_FindAll_Skip(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
{
err := table.Query().
Offset("a3").
Limit(10).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
if item.Key == "a30" || item.Key == "a3000005" {
return kvstore.Skip()
}
t.Log("key:", item.Key, "value:", item.Value)
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
}
func TestQuery_FindAll_Count(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var count int
var before = time.Now()
defer func() {
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms", "qps:", fmt.Sprintf("%.2fM/s", float64(count)/costSeconds/1_000_000))
}()
err := table.
Query().
KeysOnly().
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
count++
return true, nil
})
if err != nil {
t.Fatal(err)
}
t.Log("count:", count)
}
func TestQuery_FindAll_Field(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
defer func() {
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms", "qps:", int(1/costSeconds))
}()
var lastFieldKey []byte
t.Log("=======")
{
err := table.
Query().
FieldAsc("expiresAt").
//KeysOnly().
//FieldLt(1710848959).
Limit(3).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log(item.Key, "=>", item.Value)
lastFieldKey = item.FieldKey
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
t.Log("=======")
{
err := table.
Query().
FieldAsc("expiresAt").
//KeysOnly().
//FieldLt(1710848959).
FieldOffset(lastFieldKey).
Limit(3).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log(item.Key, "=>", item.Value)
lastFieldKey = item.FieldKey
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
}
func TestQuery_FindAll_Field_Many(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
defer func() {
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms", "qps:", int(1/costSeconds))
}()
var count = 3
if testutils.IsSingleTesting() {
count = 1_000
}
err := table.
Query().
FieldAsc("expiresAt").
KeysOnly().
Limit(count).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
t.Log(item.Key, "=>", item.Value)
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func BenchmarkQuery_FindAll(b *testing.B) {
runtime.GOMAXPROCS(4)
store, err := kvstore.OpenStore("test")
if err != nil {
b.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("db1")
if err != nil {
b.Fatal(err)
}
table, err := kvstore.NewTable[*testCachedItem]("cache_items", &testCacheItemEncoder[*testCachedItem]{})
if err != nil {
b.Fatal(err)
}
db.AddTable(table)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
err = table.Query().
//Limit(100).
FindAll(func(tx *kvstore.Tx[*testCachedItem], item kvstore.Item[*testCachedItem]) (goNext bool, err error) {
return true, nil
})
if err != nil {
b.Fatal(err)
}
}
})
}

View File

@@ -0,0 +1,258 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"errors"
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/events"
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
fsutils "github.com/TeaOSLab/EdgeNode/internal/utils/fs"
memutils "github.com/TeaOSLab/EdgeNode/internal/utils/mem"
"github.com/cockroachdb/pebble"
"github.com/iwind/TeaGo/Tea"
"io"
"os"
"path/filepath"
"strings"
"sync"
)
const StoreSuffix = ".store"
type Store struct {
name string
path string
rawDB *pebble.DB
locker *fsutils.Locker
isClosed bool
dbs []*DB
mu sync.Mutex
}
// NewStore create store with name
func NewStore(storeName string) (*Store, error) {
if !IsValidName(storeName) {
return nil, errors.New("invalid store name '" + storeName + "'")
}
var path = Tea.Root + "/data/stores/" + storeName + StoreSuffix
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(path, 0777)
}
return &Store{
name: storeName,
path: path,
locker: fsutils.NewLocker(path + "/.fs"),
}, nil
}
// NewStoreWithPath create store with path
func NewStoreWithPath(path string) (*Store, error) {
if !strings.HasSuffix(path, ".store") {
return nil, errors.New("store path must contains a '.store' suffix")
}
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(path, 0777)
}
var storeName = filepath.Base(path)
storeName = strings.TrimSuffix(storeName, ".store")
if !IsValidName(storeName) {
return nil, errors.New("invalid store name '" + storeName + "'")
}
return &Store{
name: storeName,
path: path,
locker: fsutils.NewLocker(path + "/.fs"),
}, nil
}
func OpenStore(storeName string) (*Store, error) {
store, err := NewStore(storeName)
if err != nil {
return nil, err
}
err = store.Open()
if err != nil {
return nil, err
}
return store, nil
}
func OpenStoreDir(dir string, storeName string) (*Store, error) {
if !IsValidName(storeName) {
return nil, errors.New("invalid store name '" + storeName + "'")
}
var path = strings.TrimSuffix(dir, "/") + "/" + storeName + StoreSuffix
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(path, 0777)
}
var store = &Store{
name: storeName,
path: path,
locker: fsutils.NewLocker(path + "/.fs"),
}
err = store.Open()
if err != nil {
return nil, err
}
return store, nil
}
var storeOnce = &sync.Once{}
var defaultSore *Store
func DefaultStore() (*Store, error) {
if defaultSore != nil {
return defaultSore, nil
}
var resultErr error
storeOnce.Do(func() {
store, err := NewStore("default")
if err != nil {
resultErr = fmt.Errorf("create default store failed: %w", err)
remotelogs.Error("KV", resultErr.Error())
return
}
err = store.Open()
if err != nil {
resultErr = fmt.Errorf("open default store failed: %w", err)
remotelogs.Error("KV", resultErr.Error())
return
}
defaultSore = store
})
return defaultSore, resultErr
}
func (this *Store) Path() string {
return this.path
}
func (this *Store) Open() error {
err := this.locker.Lock()
if err != nil {
return err
}
var opt = &pebble.Options{
Logger: NewLogger(),
}
if fsutils.DiskIsFast() {
opt.BytesPerSync = 1 << 20
}
var memoryMB = memutils.SystemMemoryGB() * 2
if memoryMB > 256 {
memoryMB = 256
}
if memoryMB > 4 {
opt.MemTableSize = uint64(memoryMB) << 20
}
rawDB, err := pebble.Open(this.path, opt)
if err != nil {
return err
}
this.rawDB = rawDB
// events
events.OnClose(func() {
_ = this.Close()
})
return nil
}
func (this *Store) Set(keyBytes []byte, valueBytes []byte) error {
return this.rawDB.Set(keyBytes, valueBytes, DefaultWriteOptions)
}
func (this *Store) Get(keyBytes []byte) (valueBytes []byte, closer io.Closer, err error) {
return this.rawDB.Get(keyBytes)
}
func (this *Store) Delete(keyBytes []byte) error {
return this.rawDB.Delete(keyBytes, DefaultWriteOptions)
}
func (this *Store) NewDB(dbName string) (*DB, error) {
this.mu.Lock()
defer this.mu.Unlock()
// check existence
for _, db := range this.dbs {
if db.name == dbName {
return db, nil
}
}
// create new
db, err := NewDB(this, dbName)
if err != nil {
return nil, err
}
this.dbs = append(this.dbs, db)
return db, nil
}
func (this *Store) RawDB() *pebble.DB {
return this.rawDB
}
func (this *Store) Flush() error {
return this.rawDB.Flush()
}
func (this *Store) Close() error {
if this.isClosed {
return nil
}
_ = this.locker.Release()
this.mu.Lock()
var lastErr error
for _, db := range this.dbs {
err := db.Close()
if err != nil {
lastErr = err
}
}
this.mu.Unlock()
if this.rawDB != nil {
this.isClosed = true
err := this.rawDB.Close()
if err != nil {
return err
}
}
return lastErr
}
func (this *Store) IsClosed() bool {
return this.isClosed
}

View File

@@ -0,0 +1,229 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/cockroachdb/pebble"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/assert"
_ "github.com/iwind/TeaGo/bootstrap"
"sync"
"testing"
"time"
)
func TestMain(m *testing.M) {
m.Run()
if testingStore != nil {
_ = testingStore.Close()
}
}
func TestStore_Default(t *testing.T) {
var a = assert.NewAssertion(t)
store, err := kvstore.DefaultStore()
if err != nil {
t.Fatal(err)
}
a.IsTrue(store != nil)
}
func TestStore_Default_Concurrent(t *testing.T) {
var lastStore *kvstore.Store
const threads = 32
var wg = &sync.WaitGroup{}
wg.Add(threads)
for i := 0; i < threads; i++ {
go func() {
defer wg.Done()
store, err := kvstore.DefaultStore()
if err != nil {
t.Log("ERROR", err)
t.Fail()
}
if lastStore != nil && lastStore != store {
t.Log("ERROR", "should be single instance")
t.Fail()
}
lastStore = store
}()
}
wg.Wait()
}
func TestStore_Open(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
t.Log("opened")
_ = store
}
func TestStore_Twice(t *testing.T) {
{
t.Log(1)
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
_ = store.Close()
}
{
t.Log("2")
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
}
t.Log("opened")
}
func TestStore_RawDB(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
err = store.RawDB().Set([]byte("hello"), []byte("world"), nil)
if err != nil {
t.Fatal(err)
}
}
func TestOpenStoreDir(t *testing.T) {
store, err := kvstore.OpenStoreDir(Tea.Root+"/data/stores", "test3")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
t.Log("opened")
_ = store
}
func TestStore_CloseTwice(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
for i := 0; i < 3; i++ {
err = store.Close()
if err != nil {
t.Fatal(err)
}
}
}()
}
func TestStore_Count(t *testing.T) {
testCountStore(t)
_ = testingStore.Close()
}
var testingStore *kvstore.Store
func testOpenStore(t *testing.T) *kvstore.DB {
var err error
testingStore, err = kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
db, err := testingStore.NewDB("db1")
if err != nil {
t.Fatal(err)
}
return db
}
func testOpenStoreTable[T any](t *testing.T, tableName string, encoder kvstore.ValueEncoder[T]) *kvstore.Table[T] {
var err error
var before = time.Now()
testingStore, err = kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
t.Log("store open cost:", time.Since(before).Seconds()*1000, "ms")
db, err := testingStore.NewDB("db1")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[T](tableName, encoder)
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
return table
}
func testOpenStoreTableForBenchmark[T any](t *testing.B, tableName string, encoder kvstore.ValueEncoder[T]) *kvstore.Table[T] {
var err error
testingStore, err = kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
db, err := testingStore.NewDB("db1")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[T](tableName, encoder)
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
return table
}
func testCountStore(t *testing.T) {
var err error
testingStore, err = kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
var count int
it, err := testingStore.RawDB().NewIter(&pebble.IterOptions{})
if err != nil {
t.Fatal(err)
}
defer func() {
_ = it.Close()
}()
for it.First(); it.Valid(); it.Next() {
count++
}
t.Log("count:", count)
}

View File

@@ -0,0 +1,499 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"github.com/cockroachdb/pebble"
"github.com/iwind/TeaGo/types"
"sync"
)
const (
KeyPrefix = "K$"
KeyMaxLength = 8 << 10
FieldPrefix = "F$"
MaxBatchKeys = 8 << 10 // TODO not implemented
)
type Table[T any] struct {
name string
rawNamespace []byte
db *DB
encoder ValueEncoder[T]
fieldNames []string
isClosed bool
mu *sync.RWMutex
}
func NewTable[T any](tableName string, encoder ValueEncoder[T]) (*Table[T], error) {
if !IsValidName(tableName) {
return nil, errors.New("invalid table name '" + tableName + "'")
}
return &Table[T]{
name: tableName,
encoder: encoder,
mu: &sync.RWMutex{},
}, nil
}
func (this *Table[T]) Name() string {
return this.name
}
func (this *Table[T]) Namespace() []byte {
var dest = make([]byte, len(this.rawNamespace))
copy(dest, this.rawNamespace)
return dest
}
func (this *Table[T]) SetNamespace(namespace []byte) {
this.rawNamespace = namespace
}
func (this *Table[T]) SetDB(db *DB) {
this.db = db
}
func (this *Table[T]) DB() *DB {
return this.db
}
func (this *Table[T]) Encoder() ValueEncoder[T] {
return this.encoder
}
func (this *Table[T]) Set(key string, value T) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
if len(key) > KeyMaxLength {
return ErrKeyTooLong
}
valueBytes, err := this.encoder.Encode(value)
if err != nil {
return err
}
return this.WriteTx(func(tx *Tx[T]) error {
return this.set(tx, key, valueBytes, value, false, false)
})
}
func (this *Table[T]) SetSync(key string, value T) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
if len(key) > KeyMaxLength {
return ErrKeyTooLong
}
valueBytes, err := this.encoder.Encode(value)
if err != nil {
return err
}
return this.WriteTxSync(func(tx *Tx[T]) error {
return this.set(tx, key, valueBytes, value, false, true)
})
}
func (this *Table[T]) Insert(key string, value T) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
if len(key) > KeyMaxLength {
return ErrKeyTooLong
}
valueBytes, err := this.encoder.Encode(value)
if err != nil {
return err
}
return this.WriteTx(func(tx *Tx[T]) error {
return this.set(tx, key, valueBytes, value, true, false)
})
}
// ComposeFieldKey compose field key
// $Namespace$FieldName$FieldValueSeparatorKeyValueFieldLength[2]
func (this *Table[T]) ComposeFieldKey(keyBytes []byte, fieldName string, fieldValueBytes []byte) []byte {
// TODO use 'make()' and 'copy()' to pre-alloc memory space
var b = make([]byte, 2)
binary.BigEndian.PutUint16(b, uint16(len(fieldValueBytes)))
var fieldKey = append(this.FieldKey(fieldName), '$') // namespace
fieldKey = append(fieldKey, fieldValueBytes...) // field value
fieldKey = append(fieldKey, 0, 0) // separator
fieldKey = append(fieldKey, keyBytes...) // key value
fieldKey = append(fieldKey, b...) // field value length
return fieldKey
}
func (this *Table[T]) Exist(key string) (found bool, err error) {
if this.isClosed {
return false, NewTableClosedErr(this.name)
}
_, closer, err := this.db.store.rawDB.Get(this.FullKey(key))
if err != nil {
if IsNotFound(err) {
return false, nil
}
return false, err
}
defer func() {
_ = closer.Close()
}()
return true, nil
}
func (this *Table[T]) Get(key string) (value T, err error) {
if this.isClosed {
err = NewTableClosedErr(this.name)
return
}
err = this.ReadTx(func(tx *Tx[T]) error {
resultValue, getErr := this.get(tx, key)
if getErr == nil {
value = resultValue
}
return getErr
})
return
}
func (this *Table[T]) Delete(key ...string) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
if len(key) == 0 {
return nil
}
return this.WriteTx(func(tx *Tx[T]) error {
return this.deleteKeys(tx, key...)
})
}
func (this *Table[T]) ReadTx(fn func(tx *Tx[T]) error) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
tx, err := NewTx[T](this, true)
if err != nil {
return err
}
defer func() {
_ = tx.Close()
}()
err = fn(tx)
if err != nil {
return err
}
return tx.Commit()
}
func (this *Table[T]) WriteTx(fn func(tx *Tx[T]) error) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
tx, err := NewTx[T](this, false)
if err != nil {
return err
}
defer func() {
_ = tx.Close()
}()
err = fn(tx)
if err != nil {
return err
}
return tx.Commit()
}
func (this *Table[T]) WriteTxSync(fn func(tx *Tx[T]) error) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
tx, err := NewTx[T](this, false)
if err != nil {
return err
}
defer func() {
_ = tx.Close()
}()
err = fn(tx)
if err != nil {
return err
}
return tx.CommitSync()
}
func (this *Table[T]) Truncate() error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
this.mu.Lock()
defer this.mu.Unlock()
return this.db.store.rawDB.DeleteRange(this.Namespace(), append(this.Namespace(), 0xFF), DefaultWriteOptions)
}
func (this *Table[T]) DeleteRange(start string, end string) error {
if this.isClosed {
return NewTableClosedErr(this.name)
}
return this.db.store.rawDB.DeleteRange(this.FullKeyBytes([]byte(start)), this.FullKeyBytes([]byte(end)), DefaultWriteOptions)
}
func (this *Table[T]) Query() *Query[T] {
var query = NewQuery[T]()
query.SetTable(this)
return query
}
func (this *Table[T]) Count() (int64, error) {
var count int64
var begin = this.FullKeyBytes(nil)
it, err := this.db.store.rawDB.NewIter(&pebble.IterOptions{
LowerBound: begin,
UpperBound: append(begin, 0xFF),
})
if err != nil {
return 0, err
}
defer func() {
_ = it.Close()
}()
for it.First(); it.Valid(); it.Next() {
count++
}
return count, err
}
func (this *Table[T]) FullKey(realKey string) []byte {
return append(this.Namespace(), KeyPrefix+realKey...)
}
func (this *Table[T]) FullKeyBytes(realKeyBytes []byte) []byte {
var k = append(this.Namespace(), KeyPrefix...)
k = append(k, realKeyBytes...)
return k
}
func (this *Table[T]) FieldKey(fieldName string) []byte {
var data = append(this.Namespace(), FieldPrefix...)
data = append(data, fieldName...)
return data
}
func (this *Table[T]) DecodeFieldKey(fieldName string, fieldKey []byte) (fieldValue []byte, key []byte, err error) {
var l = len(fieldKey)
var baseLen = len(this.FieldKey(fieldName)) + 1 /** $ **/ + 2 /** separator length **/ + 2 /** field length **/
if l < baseLen {
err = errors.New("invalid field key")
return
}
var fieldValueLen = binary.BigEndian.Uint16(fieldKey[l-2:])
var data = fieldKey[baseLen-4 : l-2]
fieldValue = data[:fieldValueLen]
key = data[fieldValueLen+2: /** separator length **/]
return
}
func (this *Table[T]) Close() error {
this.isClosed = true
return nil
}
func (this *Table[T]) deleteKeys(tx *Tx[T], key ...string) error {
var batch = tx.batch
for _, singleKey := range key {
var keyErr = func(singleKey string) error {
var keyBytes = this.FullKey(singleKey)
// delete field values
if len(this.fieldNames) > 0 {
valueBytes, closer, getErr := batch.Get(keyBytes)
if getErr != nil {
if IsNotFound(getErr) {
return nil
}
return getErr
}
defer func() {
_ = closer.Close()
}()
value, decodeErr := this.encoder.Decode(valueBytes)
if decodeErr != nil {
return fmt.Errorf("decode value failed: %w", decodeErr)
}
for _, fieldName := range this.fieldNames {
fieldValueBytes, fieldErr := this.encoder.EncodeField(value, fieldName)
if fieldErr != nil {
return fieldErr
}
deleteKeyErr := batch.Delete(this.ComposeFieldKey([]byte(singleKey), fieldName, fieldValueBytes), DefaultWriteOptions)
if deleteKeyErr != nil {
return deleteKeyErr
}
}
}
err := batch.Delete(keyBytes, DefaultWriteOptions)
if err != nil {
return err
}
return nil
}(singleKey)
if keyErr != nil {
return keyErr
}
}
return nil
}
func (this *Table[T]) set(tx *Tx[T], key string, valueBytes []byte, value T, insertOnly bool, syncMode bool) error {
var keyBytes = this.FullKey(key)
var writeOptions = DefaultWriteOptions
if syncMode {
writeOptions = DefaultWriteSyncOptions
}
var batch = tx.batch
// read old value
var oldValue T
var oldFound bool
var countFields = len(this.fieldNames)
if !insertOnly {
if countFields > 0 {
oldValueBytes, closer, getErr := batch.Get(keyBytes)
if getErr != nil {
if !IsNotFound(getErr) {
return getErr
}
} else {
defer func() {
_ = closer.Close()
}()
var decodeErr error
oldValue, decodeErr = this.encoder.Decode(oldValueBytes)
if decodeErr != nil {
return fmt.Errorf("decode value failed: %w", decodeErr)
}
oldFound = true
}
}
}
setErr := batch.Set(keyBytes, valueBytes, writeOptions)
if setErr != nil {
return setErr
}
// process fields
if countFields > 0 {
// add new field keys
for _, fieldName := range this.fieldNames {
// 把EncodeField放在TX里是为了节约内存
fieldValueBytes, fieldErr := this.encoder.EncodeField(value, fieldName)
if fieldErr != nil {
return fieldErr
}
if len(fieldValueBytes) > 8<<10 {
return errors.New("field value too long: " + types.String(len(fieldValueBytes)))
}
var newFieldKeyBytes = this.ComposeFieldKey([]byte(key), fieldName, fieldValueBytes)
// delete old field key
if oldFound {
oldFieldValueBytes, oldFieldErr := this.encoder.EncodeField(oldValue, fieldName)
if oldFieldErr != nil {
return oldFieldErr
}
var oldFieldKeyBytes = this.ComposeFieldKey([]byte(key), fieldName, oldFieldValueBytes)
if bytes.Equal(oldFieldKeyBytes, newFieldKeyBytes) {
// skip the field
continue
}
deleteFieldErr := batch.Delete(oldFieldKeyBytes, writeOptions)
if deleteFieldErr != nil {
return deleteFieldErr
}
}
// set new field key
setFieldErr := batch.Set(newFieldKeyBytes, nil, writeOptions)
if setFieldErr != nil {
return setFieldErr
}
}
}
return nil
}
func (this *Table[T]) get(tx *Tx[T], key string) (value T, err error) {
return this.getWithKeyBytes(tx, this.FullKey(key))
}
func (this *Table[T]) getWithKeyBytes(tx *Tx[T], keyBytes []byte) (value T, err error) {
valueBytes, closer, err := tx.batch.Get(keyBytes)
if err != nil {
return value, err
}
defer func() {
_ = closer.Close()
}()
resultValue, decodeErr := this.encoder.Decode(valueBytes)
if decodeErr != nil {
return value, fmt.Errorf("decode value failed: %w", decodeErr)
}
value = resultValue
return
}

View File

@@ -0,0 +1,38 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type CounterTable[T int64 | uint64] struct {
*Table[T]
}
func NewCounterTable[T int64 | uint64](name string) (*CounterTable[T], error) {
table, err := NewTable[T](name, NewIntValueEncoder[T]())
if err != nil {
return nil, err
}
return &CounterTable[T]{
Table: table,
}, nil
}
func (this *CounterTable[T]) Increase(key string, delta T) (newValue T, err error) {
if this.isClosed {
err = NewTableClosedErr(this.name)
return
}
err = this.Table.WriteTx(func(tx *Tx[T]) error {
value, getErr := tx.Get(key)
if getErr != nil {
if !IsNotFound(getErr) {
return getErr
}
}
newValue = value + delta
return tx.Set(key, newValue)
})
return
}

View File

@@ -0,0 +1,80 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"runtime"
"testing"
)
func TestCounterTable_Increase(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewCounterTable[uint64]("users_counter")
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
count, err := table.Increase("counter", 1)
if err != nil {
t.Fatal(err)
}
t.Log(count)
}
func BenchmarkCounterTable_Increase(b *testing.B) {
runtime.GOMAXPROCS(1)
store, err := kvstore.OpenStore("test")
if err != nil {
b.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
b.Fatal(err)
}
table, err := kvstore.NewCounterTable[uint64]("users_counter")
if err != nil {
b.Fatal(err)
}
db.AddTable(table)
defer func() {
count, incrErr := table.Increase("counter", 1)
if incrErr != nil {
b.Fatal(incrErr)
}
b.Log(count)
}()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, incrErr := table.Increase("counter", 1)
if incrErr != nil {
b.Fatal(incrErr)
}
}
})
}

View File

@@ -0,0 +1,41 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"errors"
)
func (this *Table[T]) AddField(fieldName string) error {
if !IsValidName(fieldName) {
return errors.New("invalid field name '" + fieldName + "'")
}
// check existence
for _, field := range this.fieldNames {
if field == fieldName {
return nil
}
}
this.fieldNames = append(this.fieldNames, fieldName)
return nil
}
func (this *Table[T]) AddFields(fieldName ...string) error {
for _, subFieldName := range fieldName {
err := this.AddField(subFieldName)
if err != nil {
return err
}
}
return nil
}
func (this *Table[T]) DropField(fieldName string) error {
this.mu.Lock()
defer this.mu.Unlock()
var start = this.FieldKey(fieldName + "$")
return this.db.store.rawDB.DeleteRange(start, append(start, 0xFF), DefaultWriteOptions)
}

View File

@@ -0,0 +1,271 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"encoding/binary"
"errors"
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"strconv"
"testing"
"time"
)
type testCachedItem struct {
Hash string `json:"1"` // as key
URL string `json:"2"`
ExpiresAt int64 `json:"3"`
Tag string `json:"tag"`
HeaderSize int64 `json:"headerSize"`
BodySize int64 `json:"bodySize"`
MetaSize int `json:"metaSize"`
StaleAt int64 `json:"staleAt"`
CreatedAt int64 `json:"createdAt"`
Host string `json:"host"`
ServerId int64 `json:"serverId"`
}
type testCacheItemEncoder[T interface{ *testCachedItem }] struct {
kvstore.BaseObjectEncoder[T]
}
func (this *testCacheItemEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
switch fieldName {
case "expiresAt":
var b = make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(any(value).(*testCachedItem).ExpiresAt))
return b, nil
case "staleAt":
var b = make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(any(value).(*testCachedItem).StaleAt))
return b, nil
case "url":
return []byte(any(value).(*testCachedItem).URL), nil
}
return nil, errors.New("EncodeField: invalid field name '" + fieldName + "'")
}
func TestTable_AddField(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
err := table.AddFields("expiresAt")
if err != nil {
t.Fatal(err)
}
var before = time.Now()
for _, item := range []*testCachedItem{
{
Hash: "a1",
URL: "https://example.com/a1",
ExpiresAt: 1710832067,
},
{
Hash: "a5",
URL: "https://example.com/a5",
ExpiresAt: time.Now().Unix() + 7200,
},
{
Hash: "a4",
URL: "https://example.com/a4",
ExpiresAt: time.Now().Unix() + 86400,
},
{
Hash: "a3",
URL: "https://example.com/a3",
ExpiresAt: time.Now().Unix() + 1800,
},
{
Hash: "a2",
URL: "https://example.com/a2",
ExpiresAt: time.Now().Unix() + 365*86400,
},
} {
err = table.Set(item.Hash, item)
if err != nil {
t.Fatal(err)
}
}
t.Log("set cost:", time.Since(before).Seconds()*1000, "ms")
testInspectDB(t)
}
func TestTable_AddField_Many(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
//runtime.GOMAXPROCS(1)
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
{
err := table.AddFields("expiresAt")
if err != nil {
t.Fatal(err)
}
}
{
err := table.AddFields("staleAt")
if err != nil {
t.Fatal(err)
}
}
{
err := table.AddFields("url")
if err != nil {
t.Fatal(err)
}
}
var before = time.Now()
const from = 0
const count = 4_000_000
defer func() {
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms", "qps:", int64(float64(count)/costSeconds))
}()
for i := from; i < from+count; i++ {
var item = &testCachedItem{
Hash: "a" + strconv.Itoa(i),
URL: "https://example.com/a" + strconv.Itoa(i),
ExpiresAt: 1710832067 + int64(i),
StaleAt: fasttime.Now().Unix() + int64(i),
CreatedAt: fasttime.Now().Unix(),
}
err := table.Set(item.Hash, item)
if err != nil {
t.Fatal(err)
}
}
}
func TestTable_AddField_Delete_Many(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
//runtime.GOMAXPROCS(1)
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
{
err := table.AddFields("expiresAt")
if err != nil {
t.Fatal(err)
}
}
{
err := table.AddFields("staleAt")
if err != nil {
t.Fatal(err)
}
}
{
err := table.AddFields("url")
if err != nil {
t.Fatal(err)
}
}
var before = time.Now()
const from = 0
const count = 1_000_000
for i := from; i < from+count; i++ {
var item = &testCachedItem{
Hash: "a" + strconv.Itoa(i),
}
err := table.Delete(item.Hash)
if err != nil {
t.Fatal(err)
}
}
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms", "qps:", int64(float64(count)/costSeconds))
countLeft, err := table.Count()
if err != nil {
t.Fatal(err)
}
t.Log("left:", countLeft)
}
func TestTable_DropField(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
defer func() {
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms")
}()
err := table.DropField("expiresAt")
if err != nil {
t.Fatal(err)
}
}
/**func TestTable_DeleteFieldValue(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
err := table.AddField("expiresAt")
if err != nil {
t.Fatal(err)
}
var before = time.Now()
defer func() {
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms")
}()
err = table.Delete("a2")
if err != nil {
t.Fatal(err)
}
testInspectDB(t)
}
**/
func TestTable_Inspect(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
err := table.AddFields("expiresAt")
if err != nil {
t.Fatal(err)
}
testInspectDB(t)
}

View File

@@ -0,0 +1,10 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type TableInterface interface {
Name() string
SetNamespace(namespace []byte)
SetDB(db *DB)
Close() error
}

View File

@@ -0,0 +1,417 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/assert"
"github.com/iwind/TeaGo/types"
"math/rand"
"runtime"
"strconv"
"testing"
"time"
)
func TestTable_Set(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[string]("users", kvstore.NewStringValueEncoder[string]())
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
const originValue = "b12345"
err = table.Set("a", originValue)
if err != nil {
t.Fatal(err)
}
value, err := table.Get("a")
if err != nil {
if kvstore.IsNotFound(err) {
t.Log("not found key")
return
}
t.Fatal(err)
}
t.Log("value:", value)
var a = assert.NewAssertion(t)
a.IsTrue(originValue == value)
}
func TestTable_Get(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[string]("users", kvstore.NewStringValueEncoder[string]())
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
for _, key := range []string{"a", "b", "c"} {
value, getErr := table.Get(key)
if getErr != nil {
if kvstore.IsNotFound(getErr) {
t.Log("not found key", key)
continue
}
t.Fatal(getErr)
}
t.Log(key, "=>", "value:", value)
}
}
func TestTable_Exist(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[string]("users", kvstore.NewStringValueEncoder[string]())
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
for _, key := range []string{"a", "b", "c", "12345"} {
b, checkErr := table.Exist(key)
if checkErr != nil {
t.Fatal(checkErr)
}
t.Log(key, "=>", b)
}
}
func TestTable_Delete(t *testing.T) {
var a = assert.NewAssertion(t)
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[string]("users", kvstore.NewStringValueEncoder[string]())
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
value, err := table.Get("a123")
if err != nil {
if !kvstore.IsNotFound(err) {
t.Fatal(err)
}
} else {
t.Log("old value:", value)
}
err = table.Set("a123", "123456")
if err != nil {
t.Fatal(err)
}
{
value, err = table.Get("a123")
if err != nil {
t.Fatal(err)
}
a.IsTrue(value == "123456")
}
err = table.Delete("a123")
if err != nil {
t.Fatal(err)
}
{
_, err = table.Get("a123")
a.IsTrue(kvstore.IsNotFound(err))
}
}
func TestTable_Delete_Empty(t *testing.T) {
var a = assert.NewAssertion(t)
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewTable[string]("users", kvstore.NewStringValueEncoder[string]())
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
{
err = table.Delete("a1", "a2", "a3", "a4", "")
if err != nil {
t.Fatal(err)
}
}
{
err = table.Delete()
if err != nil {
t.Fatal(err)
}
}
// set new
err = table.Set("a123", "123456")
if err != nil {
t.Fatal(err)
}
// delete again
{
err = table.Delete("a1", "a2", "a3", "a4", "")
if err != nil {
t.Fatal(err)
}
}
{
err = table.Delete()
if err != nil {
t.Fatal(err)
}
}
// read
{
var value string
value, err = table.Get("a123")
if err != nil {
t.Fatal(err)
}
a.IsTrue(value == "123456")
}
}
func TestTable_Count(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
count, err := table.Count()
if err != nil {
t.Fatal(err)
}
var costSeconds = time.Since(before).Seconds()
t.Log("count:", count, "cost:", costSeconds*1000, "ms", "qps:", fmt.Sprintf("%.2fM/s", float64(count)/costSeconds/1_000_000))
// watch memory usage
if testutils.IsSingleTesting() {
//time.Sleep(5 * time.Minute)
}
}
func TestTable_Truncate(t *testing.T) {
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var before = time.Now()
err := table.Truncate()
if err != nil {
t.Fatal(err)
}
var costSeconds = time.Since(before).Seconds()
t.Log("cost:", costSeconds*1000, "ms")
t.Log("===after truncate===")
testInspectDB(t)
}
func TestTable_ComposeFieldKey(t *testing.T) {
var a = assert.NewAssertion(t)
var table = testOpenStoreTable[*testCachedItem](t, "cache_items", &testCacheItemEncoder[*testCachedItem]{})
defer func() {
_ = testingStore.Close()
}()
var fieldKeyBytes = table.ComposeFieldKey([]byte("Lily"), "username", []byte("lucy"))
t.Log(string(fieldKeyBytes))
fieldValueBytes, keyValueBytes, err := table.DecodeFieldKey("username", fieldKeyBytes)
if err != nil {
t.Fatal(err)
}
t.Log("field:", string(fieldValueBytes), "key:", string(keyValueBytes))
a.IsTrue(string(fieldValueBytes) == "lucy")
a.IsTrue(string(keyValueBytes) == "Lily")
}
func BenchmarkTable_Set(b *testing.B) {
runtime.GOMAXPROCS(4)
store, err := kvstore.OpenStore("test")
if err != nil {
b.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
b.Fatal(err)
}
table, err := kvstore.NewTable[uint8]("users", kvstore.NewIntValueEncoder[uint8]())
if err != nil {
b.Fatal(err)
}
db.AddTable(table)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
putErr := table.Set(strconv.Itoa(rand.Int()), 1)
if putErr != nil {
b.Fatal(putErr)
}
}
})
}
func BenchmarkTable_Get(b *testing.B) {
runtime.GOMAXPROCS(4)
store, err := kvstore.OpenStore("test")
if err != nil {
b.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
b.Fatal(err)
}
table, err := kvstore.NewTable[uint8]("users", kvstore.NewIntValueEncoder[uint8]())
if err != nil {
b.Fatal(err)
}
db.AddTable(table)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, putErr := table.Get(types.String(rand.Int()))
if putErr != nil {
if kvstore.IsNotFound(putErr) {
continue
}
b.Fatal(putErr)
}
}
})
}
/**func BenchmarkTable_NextId(b *testing.B) {
runtime.GOMAXPROCS(4)
store, err := kvstore.OpenStore("test")
if err != nil {
b.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
b.Fatal(err)
}
table, err := kvstore.NewTable[uint8]("users", kvstore.NewIntValueEncoder[uint8]())
if err != nil {
b.Fatal(err)
}
db.AddTable(table)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, nextErr := table.NextId("a")
if nextErr != nil {
b.Fatal(nextErr)
}
}
})
}
**/

View File

@@ -0,0 +1,140 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"errors"
"fmt"
"github.com/cockroachdb/pebble"
)
type Tx[T any] struct {
table *Table[T]
readOnly bool
batch *pebble.Batch
}
func NewTx[T any](table *Table[T], readOnly bool) (*Tx[T], error) {
if table.db == nil {
return nil, errors.New("the table has not been added to a db")
}
if table.db.store == nil {
return nil, errors.New("the db has not been added to a store")
}
if table.db.store.rawDB == nil {
return nil, errors.New("the store has not been opened")
}
return &Tx[T]{
table: table,
readOnly: readOnly,
batch: table.db.store.rawDB.NewIndexedBatch(),
}, nil
}
func (this *Tx[T]) Set(key string, value T) error {
if this.readOnly {
return errors.New("can not set value in readonly transaction")
}
if len(key) > KeyMaxLength {
return ErrKeyTooLong
}
valueBytes, err := this.table.encoder.Encode(value)
if err != nil {
return err
}
return this.table.set(this, key, valueBytes, value, false, false)
}
func (this *Tx[T]) SetSync(key string, value T) error {
if this.readOnly {
return errors.New("can not set value in readonly transaction")
}
if len(key) > KeyMaxLength {
return ErrKeyTooLong
}
valueBytes, err := this.table.encoder.Encode(value)
if err != nil {
return err
}
return this.table.set(this, key, valueBytes, value, false, true)
}
func (this *Tx[T]) Insert(key string, value T) error {
if this.readOnly {
return errors.New("can not set value in readonly transaction")
}
if len(key) > KeyMaxLength {
return ErrKeyTooLong
}
valueBytes, err := this.table.encoder.Encode(value)
if err != nil {
return err
}
return this.table.set(this, key, valueBytes, value, true, false)
}
func (this *Tx[T]) Get(key string) (value T, err error) {
if this.table.isClosed {
err = NewTableClosedErr(this.table.name)
return
}
return this.table.get(this, key)
}
func (this *Tx[T]) Delete(key string) error {
if this.table.isClosed {
return NewTableClosedErr(this.table.name)
}
if this.readOnly {
return errors.New("can not delete value in readonly transaction")
}
return this.table.deleteKeys(this, key)
}
func (this *Tx[T]) NewIterator(opt *IteratorOptions) (*pebble.Iterator, error) {
return this.batch.NewIter(opt.RawOptions())
}
func (this *Tx[T]) Close() error {
return this.batch.Close()
}
func (this *Tx[T]) Commit() (err error) {
return this.commit(DefaultWriteOptions)
}
func (this *Tx[T]) CommitSync() (err error) {
return this.commit(DefaultWriteSyncOptions)
}
func (this *Tx[T]) Query() *Query[T] {
var query = NewQuery[T]()
query.SetTx(this)
return query
}
func (this *Tx[T]) commit(opt *pebble.WriteOptions) (err error) {
defer func() {
var panicErr = recover()
if panicErr != nil {
resultErr, ok := panicErr.(error)
if ok {
err = fmt.Errorf("commit batch failed: %w", resultErr)
}
}
}()
return this.batch.Commit(opt)
}

View File

@@ -0,0 +1,65 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"fmt"
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"testing"
)
func TestTable_ReadTx(t *testing.T) {
store, err := kvstore.OpenStore("test")
if err != nil {
t.Fatal(err)
}
defer func() {
_ = store.Close()
}()
db, err := store.NewDB("TEST_DB")
if err != nil {
t.Fatal(err)
}
table, err := kvstore.NewCounterTable[uint64]("users_counter")
if err != nil {
t.Fatal(err)
}
db.AddTable(table)
err = table.WriteTx(func(tx *kvstore.Tx[uint64]) error {
for i := 0; i < 1000; i++ {
var key = fmt.Sprintf("a%03d", i)
setErr := tx.Set(key, uint64(i))
if setErr != nil {
return setErr
}
value, getErr := tx.Get(key)
if getErr != nil {
return getErr
}
t.Log("write:", key, "=>", value)
}
return nil
})
if err != nil {
t.Fatal(err)
}
err = table.ReadTx(func(tx *kvstore.Tx[uint64]) error {
for _, key := range []string{"a100", "a101", "a102"} {
value, getErr := tx.Get(key)
if getErr != nil {
return getErr
}
t.Log("read:", key, "=>", value)
}
return nil
})
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"errors"
"os"
"regexp"
"strings"
)
var nameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
// IsValidName check if store name or database name or table name is valid
func IsValidName(name string) bool {
return nameRegexp.MatchString(name)
}
// RemoveStore remove store directory
func RemoveStore(path string) error {
var errNotStoreDirectory = errors.New("not store directory")
if strings.HasSuffix(path, StoreSuffix) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
// validate store
{
_, err = os.Stat(path + "/CURRENT")
if err != nil {
return errNotStoreDirectory
}
}
{
_, err = os.Stat(path + "/LOCK")
if err != nil {
return errNotStoreDirectory
}
}
return os.RemoveAll(path)
}
return errNotStoreDirectory
}

View File

@@ -0,0 +1,29 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/iwind/TeaGo/Tea"
"github.com/iwind/TeaGo/assert"
_ "github.com/iwind/TeaGo/bootstrap"
"testing"
)
func TestRemoveDB(t *testing.T) {
err := kvstore.RemoveStore(Tea.Root + "/data/stores/test2.store")
if err != nil {
t.Fatal(err)
}
}
func TestIsValidName(t *testing.T) {
var a = assert.NewAssertion(t)
a.IsTrue(kvstore.IsValidName("a"))
a.IsTrue(kvstore.IsValidName("aB"))
a.IsTrue(kvstore.IsValidName("aBC1"))
a.IsTrue(kvstore.IsValidName("aBC1._-"))
a.IsFalse(kvstore.IsValidName(" aBC1._-"))
a.IsFalse(kvstore.IsValidName(""))
}

View File

@@ -0,0 +1,80 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import (
"encoding/binary"
"github.com/iwind/TeaGo/types"
"golang.org/x/exp/constraints"
"strconv"
)
type IntValueEncoder[T constraints.Integer] struct {
}
func NewIntValueEncoder[T constraints.Integer]() *IntValueEncoder[T] {
return &IntValueEncoder[T]{}
}
func (this *IntValueEncoder[T]) Encode(value T) (data []byte, err error) {
switch v := any(value).(type) {
case int8, int16, int32, int, uint:
data = []byte(types.String(v))
case int64:
data = []byte(strconv.FormatInt(v, 16))
case uint8:
return []byte{v}, nil
case uint16:
data = make([]byte, 2)
binary.BigEndian.PutUint16(data, v)
case uint32:
data = make([]byte, 4)
binary.BigEndian.PutUint32(data, v)
case uint64:
data = make([]byte, 8)
binary.BigEndian.PutUint64(data, v)
}
return
}
func (this *IntValueEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
_ = fieldName
return this.Encode(value)
}
func (this *IntValueEncoder[T]) Decode(valueData []byte) (value T, err error) {
switch any(value).(type) {
case int8:
value = T(types.Int8(string(valueData)))
case int16:
value = T(types.Int16(string(valueData)))
case int32:
value = T(types.Int32(string(valueData)))
case int64:
int64Value, _ := strconv.ParseInt(string(valueData), 16, 64)
value = T(int64Value)
case int:
value = T(types.Int(string(valueData)))
case uint:
value = T(types.Uint(string(valueData)))
case uint8:
if len(valueData) == 1 {
value = T(valueData[0])
}
case uint16:
if len(valueData) == 2 {
value = T(binary.BigEndian.Uint16(valueData))
}
case uint32:
if len(valueData) == 4 {
value = T(binary.BigEndian.Uint32(valueData))
}
case uint64:
if len(valueData) == 8 {
value = T(binary.BigEndian.Uint64(valueData))
}
}
return
}

View File

@@ -0,0 +1,23 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
import "encoding/json"
type ValueEncoder[T any] interface {
Encode(value T) ([]byte, error)
EncodeField(value T, fieldName string) ([]byte, error)
Decode(valueBytes []byte) (value T, err error)
}
type BaseObjectEncoder[T any] struct {
}
func (this *BaseObjectEncoder[T]) Encode(value T) ([]byte, error) {
return json.Marshal(value)
}
func (this *BaseObjectEncoder[T]) Decode(valueData []byte) (value T, err error) {
err = json.Unmarshal(valueData, &value)
return
}

View File

@@ -0,0 +1,29 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type BoolValueEncoder[T bool] struct {
}
func NewBoolValueEncoder[T bool]() *BoolValueEncoder[T] {
return &BoolValueEncoder[T]{}
}
func (this *BoolValueEncoder[T]) Encode(value T) ([]byte, error) {
if value {
return []byte{1}, nil
}
return []byte{0}, nil
}
func (this *BoolValueEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
_ = fieldName
return this.Encode(value)
}
func (this *BoolValueEncoder[T]) Decode(valueData []byte) (value T, err error) {
if len(valueData) == 1 {
value = valueData[0] == 1
}
return
}

View File

@@ -0,0 +1,35 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type BytesValueEncoder[T []byte] struct {
}
func NewBytesValueEncoder[T []byte]() *BytesValueEncoder[T] {
return &BytesValueEncoder[T]{}
}
func (this *BytesValueEncoder[T]) Encode(value T) ([]byte, error) {
if len(value) == 0 {
return nil, nil
}
var resultValue = make([]byte, len(value))
copy(resultValue, value)
return resultValue, nil
}
func (this *BytesValueEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
_ = fieldName
return this.Encode(value)
}
func (this *BytesValueEncoder[T]) Decode(valueData []byte) (value T, err error) {
if len(valueData) == 0 {
return
}
value = make([]byte, len(valueData))
copy(value, valueData)
return
}

View File

@@ -0,0 +1,22 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type NilValueEncoder[T []byte] struct {
}
func NewNilValueEncoder[T []byte]() *NilValueEncoder[T] {
return &NilValueEncoder[T]{}
}
func (this *NilValueEncoder[T]) Encode(value T) ([]byte, error) {
return nil, nil
}
func (this *NilValueEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
return nil, nil
}
func (this *NilValueEncoder[T]) Decode(valueData []byte) (value T, err error) {
return
}

View File

@@ -0,0 +1,24 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore
type StringValueEncoder[T string] struct {
}
func NewStringValueEncoder[T string]() *StringValueEncoder[T] {
return &StringValueEncoder[T]{}
}
func (this *StringValueEncoder[T]) Encode(value T) ([]byte, error) {
return []byte(value), nil
}
func (this *StringValueEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
_ = fieldName
return this.Encode(value)
}
func (this *StringValueEncoder[T]) Decode(valueData []byte) (value T, err error) {
value = T(valueData)
return
}

View File

@@ -0,0 +1,469 @@
// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package kvstore_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/kvstore"
"github.com/iwind/TeaGo/assert"
"testing"
)
func TestStringValueEncoder_Encode(t *testing.T) {
var a = assert.NewAssertion(t)
var encoder = kvstore.NewStringValueEncoder[string]()
data, err := encoder.Encode("abcdefg")
if err != nil {
t.Fatal(err)
}
value, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(value == "abcdefg")
}
func TestIntValueEncoder_Encode(t *testing.T) {
var a = assert.NewAssertion(t)
{
var encoder = kvstore.NewIntValueEncoder[int8]()
data, err := encoder.Encode(1)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 1)
t.Log("int8", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int8]()
data, err := encoder.Encode(-1)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == -1)
t.Log("int8", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int16]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("int16", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int16]()
data, err := encoder.Encode(-123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == -123)
t.Log("int16", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int32]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("int32", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int32]()
data, err := encoder.Encode(-123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == -123)
t.Log("int32", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int64]()
data, err := encoder.Encode(123456)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123456)
t.Log("int64", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int64]()
data, err := encoder.Encode(1234567890)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 1234567890)
t.Log("int64", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int64]()
data, err := encoder.Encode(-123456)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == -123456)
t.Log("int64", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("int", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[int]()
data, err := encoder.Encode(-123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == -123)
t.Log("int", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[uint]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("uint", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[uint8]()
data, err := encoder.Encode(97)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 97)
t.Log("uint8", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[uint16]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("uint16", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[uint32]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("uint32", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[uint64]()
data, err := encoder.Encode(123)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 123)
t.Log("uint64", string(data), "=>", data, "=>", v)
}
{
var encoder = kvstore.NewIntValueEncoder[uint64]()
data, err := encoder.Encode(1234567890)
if err != nil {
t.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
a.IsTrue(v == 1234567890)
t.Log("uint64", string(data), "=>", data, "=>", v)
}
}
func TestBytesValueEncoder_Encode(t *testing.T) {
var encoder = kvstore.NewBytesValueEncoder[[]byte]()
{
data, err := encoder.Encode(nil)
if err != nil {
t.Fatal(err)
}
value, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
t.Log(data, "=>", value)
}
{
data, err := encoder.Encode([]byte("ABC"))
if err != nil {
t.Fatal(err)
}
value, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
t.Log(data, "=>", value)
}
}
func TestBytesValueEncoder_Bool(t *testing.T) {
var encoder = kvstore.NewBoolValueEncoder[bool]()
{
data, err := encoder.Encode(true)
if err != nil {
t.Fatal(err)
}
value, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
t.Log(data, "=>", value)
}
{
data, err := encoder.Encode(false)
if err != nil {
t.Fatal(err)
}
value, err := encoder.Decode(data)
if err != nil {
t.Fatal(err)
}
t.Log(data, "=>", value)
}
{
value, err := encoder.Decode(nil)
if err != nil {
t.Fatal(err)
}
t.Log("nil", "=>", value)
}
{
value, err := encoder.Decode([]byte{1, 2, 3, 4})
if err != nil {
t.Fatal(err)
}
t.Log("nil", "=>", value)
}
}
type objectType struct {
Name string `json:"1"`
Age int `json:"2"`
}
type objectTypeEncoder[T objectType] struct {
kvstore.BaseObjectEncoder[T]
}
func (this *objectTypeEncoder[T]) EncodeField(value T, fieldName string) ([]byte, error) {
return nil, nil
}
func TestBaseObjectEncoder_Encode(t *testing.T) {
var encoder = &objectTypeEncoder[objectType]{}
{
data, err := encoder.Encode(objectType{
Name: "lily",
Age: 20,
})
if err != nil {
t.Fatal(err)
}
t.Log("encoded:", string(data))
}
{
value, err := encoder.Decode([]byte(`{"1":"lily","2":20}`))
if err != nil {
t.Fatal(err)
}
t.Logf("decoded: %+v", value)
}
}
type objectType2 struct {
Name string `json:"1"`
Age int `json:"2"`
}
type objectTypeEncoder2[T interface{ *objectType2 }] struct {
kvstore.BaseObjectEncoder[T]
}
func (this *objectTypeEncoder2[T]) EncodeField(value T, fieldName string) ([]byte, error) {
switch fieldName {
case "Name":
return []byte(any(value).(*objectType2).Name), nil
}
return nil, nil
}
func TestBaseObjectEncoder_Encode2(t *testing.T) {
var encoder = &objectTypeEncoder2[*objectType2]{}
{
data, err := encoder.Encode(&objectType2{
Name: "lily",
Age: 20,
})
if err != nil {
t.Fatal(err)
}
t.Log("encoded:", string(data))
}
{
value, err := encoder.Decode([]byte(`{"1":"lily","2":20}`))
if err != nil {
t.Fatal(err)
}
t.Logf("decoded: %+v", value)
}
{
field, err := encoder.EncodeField(&objectType2{
Name: "lily",
Age: 20,
}, "Name")
if err != nil {
t.Fatal(err)
}
t.Log("encoded field:", string(field))
}
}
func BenchmarkStringValueEncoder_Encode(b *testing.B) {
for i := 0; i < b.N; i++ {
var encoder = kvstore.NewStringValueEncoder[string]()
data, err := encoder.Encode("1234567890")
if err != nil {
b.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
b.Fatal(err)
}
_ = v
}
}
func BenchmarkIntValueEncoder_Encode(b *testing.B) {
for i := 0; i < b.N; i++ {
var encoder = kvstore.NewIntValueEncoder[int64]()
data, err := encoder.Encode(1234567890)
if err != nil {
b.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
b.Fatal(err)
}
_ = v
}
}
func BenchmarkUIntValueEncoder_Encode(b *testing.B) {
for i := 0; i < b.N; i++ {
var encoder = kvstore.NewIntValueEncoder[uint64]()
data, err := encoder.Encode(1234567890)
if err != nil {
b.Fatal(err)
}
v, err := encoder.Decode(data)
if err != nil {
b.Fatal(err)
}
_ = v
}
}