From ed9af2b4593bdb1500fc146a6298fa351d743bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=D1=91m=20Mishin?= Date: Fri, 25 Apr 2025 12:51:59 +0300 Subject: [PATCH] feat: Initial --- .idea/.gitignore | 8 +++ .idea/modules.xml | 8 +++ .idea/universe_badger.iml | 9 +++ .idea/vcs.xml | 4 ++ go.mod | 31 +++++++++ go.sum | 61 +++++++++++++++++ kv_provider.go | 32 +++++++++ kv_store.go | 138 ++++++++++++++++++++++++++++++++++++++ logger.go | 31 +++++++++ module.go | 54 +++++++++++++++ 10 files changed, 376 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/universe_badger.iml create mode 100644 .idea/vcs.xml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 kv_provider.go create mode 100644 kv_store.go create mode 100644 logger.go create mode 100644 module.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..65ec4c6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/universe_badger.iml b/.idea/universe_badger.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/universe_badger.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..94d4d9d --- /dev/null +++ b/go.mod @@ -0,0 +1,31 @@ +module go.mrlokop.dev/universe/data/badger + +go 1.24 + +require ( + github.com/dgraph-io/badger/v4 v4.7.0 + go.uber.org/fx v1.23.0 + go.uber.org/zap v1.27.0 + universe.mrlokop.dev/core v0.0.0-20250425091205-db1d0545d9c6 + universe.mrlokop.dev/key_value v0.0.0-20250425093329-a79c041fe7e1 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/dig v1.18.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0a94e9f --- /dev/null +++ b/go.sum @@ -0,0 +1,61 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= +github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es= +go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +universe.mrlokop.dev/core v0.0.0-20250425091205-db1d0545d9c6 h1:aM0o2QFSizLY9KOXuoOmsp/o345yJEptgq8egA85VnM= +universe.mrlokop.dev/core v0.0.0-20250425091205-db1d0545d9c6/go.mod h1:1xEErU5FKve38at9zd2RZDDbojuebd9rGpWzeUD4zu4= +universe.mrlokop.dev/key_value v0.0.0-20250425091341-1d08501b51d7 h1:jJmVvb+UVioVyJpYe5jF3W0iCxNF+RR4uR4h695aYVI= +universe.mrlokop.dev/key_value v0.0.0-20250425091341-1d08501b51d7/go.mod h1:ajRUrvqBdKaLg9vINxxCEYme+DXfzotA+HIfje+mmZM= +universe.mrlokop.dev/key_value v0.0.0-20250425093329-a79c041fe7e1 h1:dpEjqVgueSxw+G73g9g80B0yhJKIu4zWwO38ovbRoiE= +universe.mrlokop.dev/key_value v0.0.0-20250425093329-a79c041fe7e1/go.mod h1:WNqaFQruy5ich5wzhivNQf6PljJ9QxiBJ8gmvJMemG4= diff --git a/kv_provider.go b/kv_provider.go new file mode 100644 index 0000000..ab57ef6 --- /dev/null +++ b/kv_provider.go @@ -0,0 +1,32 @@ +package universe_badger + +import ( + "github.com/dgraph-io/badger/v4" + kv "universe.mrlokop.dev/key_value" +) + +type keyValueProvider struct { + db *badger.DB +} + +func (k *keyValueProvider) HasCapability(capability kv.Capability) bool { + switch capability { + default: + return false + } +} + +func (k *keyValueProvider) ProvideBinary(prefix string) (kv.KeyValueStoreBinary, error) { + return &keyValueStorage{ + db: k.db, + prefix: prefix, + }, nil +} + +func (k *keyValueProvider) ProvideString(prefix string) (kv.KeyValueStoreString, error) { + bin, err := k.ProvideBinary(prefix) + if err != nil { + return nil, err + } + return kv.WrapStringToBinary(bin), err +} diff --git a/kv_store.go b/kv_store.go new file mode 100644 index 0000000..1890fc6 --- /dev/null +++ b/kv_store.go @@ -0,0 +1,138 @@ +package universe_badger + +import ( + "context" + "github.com/dgraph-io/badger/v4" + "time" + "universe.mrlokop.dev/core/errors" + kv "universe.mrlokop.dev/key_value" +) + +func wrapErr(err error) error { + if errors.Is(err, badger.ErrKeyNotFound) { + return kv.ErrNoSuchKey.CreateWithCause(err) + } + return err +} + +type keyValueStorage struct { + db *badger.DB + prefix string +} + +func (k *keyValueStorage) Get(ctx context.Context, key string) ([]byte, error) { + var result []byte + err := k.db.View(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + item, err := txn.Get(keyBytes) + if err != nil { + return wrapErr(err) + } + result = make([]byte, item.ValueSize()) + _, err = item.ValueCopy(result) + return wrapErr(err) + }) + return result, err +} + +func (k *keyValueStorage) Has(ctx context.Context, key string) (bool, error) { + var exists bool + err := k.db.View(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + _, err := txn.Get(keyBytes) + if err != nil { + if errors.Is(err, badger.ErrKeyNotFound) { + exists = false + return nil + } + return wrapErr(err) + } + exists = true + return nil + }) + return exists, err +} + +func (k *keyValueStorage) Set(ctx context.Context, key string, value []byte) error { + return k.db.Update(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + return wrapErr(txn.Set(keyBytes, value)) + }) +} + +func (k *keyValueStorage) SetWithExpiration(ctx context.Context, key string, value []byte, expiresAt time.Time) error { + return k.db.Update(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + entry := badger.NewEntry(keyBytes, value) + entry.ExpiresAt = uint64(expiresAt.Unix()) + return wrapErr(txn.SetEntry(entry)) + }) +} + +func (k *keyValueStorage) SetIfNotExist(ctx context.Context, key string, value []byte) (bool, error) { + var wasSet bool + err := k.db.Update(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + _, err := txn.Get(keyBytes) + if err != nil { + if errors.Is(err, badger.ErrKeyNotFound) { + wasSet = true + return wrapErr(txn.Set(keyBytes, value)) + } + } + return wrapErr(err) + }) + return wasSet, wrapErr(err) +} + +func (k *keyValueStorage) SetIfNotExistWithExpiration(ctx context.Context, key string, value []byte, expiration time.Time) (bool, error) { + var wasSet bool + err := k.db.Update(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + _, err := txn.Get(keyBytes) + if err != nil { + if errors.Is(err, badger.ErrKeyNotFound) { + wasSet = true + entry := badger.NewEntry(keyBytes, value) + entry.ExpiresAt = uint64(expiration.Unix()) + return wrapErr(txn.SetEntry(entry)) + } + } + return wrapErr(err) + }) + return wasSet, err +} + +func (k *keyValueStorage) ExpireAt(ctx context.Context, key string, expiration time.Time) error { + return k.db.Update(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + item, err := txn.Get(keyBytes) + if err != nil { + return wrapErr(err) + } + + copyOfBytes := make([]byte, item.ValueSize()) + _, err = item.ValueCopy(copyOfBytes) + if err != nil { + return wrapErr(err) + } + + entry := badger.NewEntry(keyBytes, copyOfBytes) + entry.ExpiresAt = uint64(expiration.Unix()) + return wrapErr(txn.SetEntry(entry)) + }) +} + +func (k *keyValueStorage) Remove(ctx context.Context, key string) error { + return k.db.Update(func(txn *badger.Txn) error { + keyBytes := []byte(k.prefix + key) + return wrapErr(txn.Delete(keyBytes)) + }) +} + +func (k *keyValueStorage) HasCapability(capability kv.Capability) bool { + switch capability { + default: + return false + } +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..48cff61 --- /dev/null +++ b/logger.go @@ -0,0 +1,31 @@ +package universe_badger + +import ( + "fmt" + "go.uber.org/zap" + "strings" +) + +type zapRedirectingLogger struct { + target *zap.Logger +} + +func (z *zapRedirectingLogger) Errorf(s string, i ...interface{}) { + s, _ = strings.CutSuffix(s, "\n") + z.target.Error(fmt.Sprintf(s, i...)) +} + +func (z *zapRedirectingLogger) Warningf(s string, i ...interface{}) { + s, _ = strings.CutSuffix(s, "\n") + z.target.Warn(fmt.Sprintf(s, i...)) +} + +func (z *zapRedirectingLogger) Infof(s string, i ...interface{}) { + s, _ = strings.CutSuffix(s, "\n") + z.target.Info(fmt.Sprintf(s, i...)) +} + +func (z *zapRedirectingLogger) Debugf(s string, i ...interface{}) { + s, _ = strings.CutSuffix(s, "\n") + z.target.Debug(fmt.Sprintf(s, i...)) +} diff --git a/module.go b/module.go new file mode 100644 index 0000000..6583554 --- /dev/null +++ b/module.go @@ -0,0 +1,54 @@ +package universe_badger + +import ( + "github.com/dgraph-io/badger/v4" + "go.uber.org/fx" + "go.uber.org/zap" + um "universe.mrlokop.dev/core/modules" + kv "universe.mrlokop.dev/key_value" +) + +type moduleConfig struct { + Path string +} + +var ( + Module = um.SimpleModule("universe:badger", func(ctx um.ModuleFactoryContext) error { + cfg := &moduleConfig{} + err := ctx.ExtractConfig(cfg) + if err != nil { + return err + } + + ctx.Provide(fx.Annotate(func( + logger *zap.Logger, + ) (*badger.DB, error) { + var ( + ll = ctx.ConfigureLogger(logger) + opts = badger.DefaultOptions(cfg.Path) + ) + + if cfg.Path == "" { + opts.WithInMemory(true) + } + + opts.WithLogger(&zapRedirectingLogger{target: ll}) + + db, err := badger.Open(opts) + if err != nil { + return nil, err + } + + return db, nil + }, fx.ResultTags(ctx.NameTag()))) + + ctx.Provide(fx.Annotate(func( + badger *badger.DB, + logger *zap.Logger, + ) kv.KeyValueProvider { + return &keyValueProvider{} + }, fx.ParamTags(ctx.NameTag()), fx.ResultTags(ctx.NameTag()))) + + return nil + }) +)