migrated to leveldb instead of pebble

This commit is contained in:
d1nch8g
2026-05-10 23:27:41 +07:00
parent f8a42ed167
commit 765a6696d1
24 changed files with 808 additions and 825 deletions
+6 -6
View File
@@ -42,7 +42,7 @@ type User struct {
Bio string `json:"bio" gob:"3"`
PublicKey []byte `json:"publicKey" gob:"4"`
Avatar uuid.UUID `json:"avatar" gob:"5"`
Subscribers int `json:"subscribers" gob:"-"`
Subscribers uint64 `json:"subscribers" gob:"-"`
}
type Subscriptions interface {
@@ -62,14 +62,14 @@ type Subscription struct {
type SubscriptionReference struct {
Domain string `json:"domain"`
Count int `json:"count"`
Count uint64 `json:"count"`
}
type Files interface {
Put(r io.Reader) (uuid.UUID, error)
Get(id uuid.UUID, w io.Writer) error
Delete(id uuid.UUID) error
Size(id uuid.UUID) (int64, error)
Put(user string, r io.Reader) (uuid.UUID, error)
Get(user string, id uuid.UUID, w io.Writer) error
Delete(user string, id uuid.UUID) error
Size(user string, id uuid.UUID) (int64, error)
}
type Emails interface {
+7
View File
@@ -0,0 +1,7 @@
package leveldb
import "github.com/syndtr/goleveldb/leveldb"
type comments struct {
db *leveldb.DB
}
+9
View File
@@ -0,0 +1,9 @@
package leveldb
import (
"github.com/syndtr/goleveldb/leveldb"
)
type emails struct {
db *leveldb.DB
}
+132
View File
@@ -0,0 +1,132 @@
package leveldb
import (
"encoding/binary"
"fmt"
"io"
"github.com/d1nch8g/mesh/database"
"github.com/google/uuid"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type files struct {
db *leveldb.DB
}
const fileChunkSize = 4 << 20 // 4 MB
func (f *files) Put(user string, r io.Reader) (uuid.UUID, error) {
id := uuid.New()
batch := new(leveldb.Batch)
defer batch.Reset()
var totalSize uint64
chunk := make([]byte, fileChunkSize)
var chunkIndex uint32
hasData := false
for {
n, err := r.Read(chunk)
if n > 0 {
var idxBuf [4]byte
binary.BigEndian.PutUint32(idxBuf[:], chunkIndex)
k := key(user, prefixFileChunk, id, idxBuf[:])
buf := make([]byte, n)
copy(buf, chunk[:n])
batch.Put(k, buf)
totalSize += uint64(n)
chunkIndex++
hasData = true
}
if err == io.EOF {
break
}
if err != nil {
return uuid.Nil, fmt.Errorf("read file %q: %w", id, err)
}
}
if !hasData {
var idxBuf [4]byte
binary.BigEndian.PutUint32(idxBuf[:], 0)
k := key(user, prefixFileChunk, id, idxBuf[:])
batch.Put(k, []byte{})
}
metaKey := key(user, prefixFileMeta, id)
var sizeBuf [8]byte
binary.LittleEndian.PutUint64(sizeBuf[:], totalSize)
batch.Put(metaKey, sizeBuf[:])
if err := f.db.Write(batch, nil); err != nil {
return uuid.Nil, fmt.Errorf("commit file %q: %w", id, err)
}
return id, nil
}
func (f *files) Get(user string, id uuid.UUID, w io.Writer) error {
prefix := key(user, prefixFileChunk, id)
iter := f.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
chunkCount := 0
for iter.Next() {
if _, err := w.Write(iter.Value()); err != nil {
return fmt.Errorf("write file %q: %w", id, err)
}
chunkCount++
}
if chunkCount == 0 {
return database.ErrNotFound
}
return nil
}
func (f *files) Delete(user string, id uuid.UUID) error {
prefix := key(user, prefixFileChunk, id)
iter := f.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
if !iter.Next() {
return database.ErrNotFound
}
batch := new(leveldb.Batch)
defer batch.Reset()
iter.Prev()
for iter.Next() {
batch.Delete(iter.Key())
}
metaKey := key(user, prefixFileMeta, id)
batch.Delete(metaKey)
if err := f.db.Write(batch, nil); err != nil {
return fmt.Errorf("commit delete %q: %w", id, err)
}
return nil
}
func (f *files) Size(user string, id uuid.UUID) (int64, error) {
metaKey := key(user, prefixFileMeta, id)
data, err := f.db.Get(metaKey, nil)
if err != nil {
if err == leveldb.ErrNotFound {
return 0, database.ErrNotFound
}
return 0, fmt.Errorf("size file %q: %w", id, err)
}
return int64(binary.LittleEndian.Uint64(data)), nil
}
+264
View File
@@ -0,0 +1,264 @@
package leveldb
import (
"bytes"
"os"
"testing"
"github.com/d1nch8g/mesh/database"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func openDB(t *testing.T) (*DB, func()) {
t.Helper()
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
return db, func() { os.RemoveAll(path); db.Close() }
}
func TestFiles_Put(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
user := "masha@d1.com"
t.Run("put small file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("hello")))
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, id)
size, err := files.Size(user, id)
require.NoError(t, err)
require.Equal(t, int64(5), size)
})
t.Run("put empty file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte{}))
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, id)
size, err := files.Size(user, id)
require.NoError(t, err)
require.Equal(t, int64(0), size)
})
t.Run("put file larger than one chunk", func(t *testing.T) {
data := bytes.Repeat([]byte("a"), fileChunkSize+1)
id, err := files.Put(user, bytes.NewReader(data))
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, id)
size, err := files.Size(user, id)
require.NoError(t, err)
require.Equal(t, int64(len(data)), size)
})
t.Run("put multiple files generates different ids", func(t *testing.T) {
id1, err := files.Put(user, bytes.NewReader([]byte("first")))
require.NoError(t, err)
id2, err := files.Put(user, bytes.NewReader([]byte("second")))
require.NoError(t, err)
require.NotEqual(t, id1, id2)
})
t.Run("files isolated between users", func(t *testing.T) {
id1, err := files.Put(user, bytes.NewReader([]byte("user1-data")))
require.NoError(t, err)
id2, err := files.Put("petya@d2.io", bytes.NewReader([]byte("user2-data")))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(user, id1, &buf)
require.NoError(t, err)
require.Equal(t, "user1-data", buf.String())
buf.Reset()
err = files.Get("petya@d2.io", id2, &buf)
require.NoError(t, err)
require.Equal(t, "user2-data", buf.String())
})
}
func TestFiles_Get(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
user := "masha@d1.com"
t.Run("get existing file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("hello world")))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(user, id, &buf)
require.NoError(t, err)
require.Equal(t, "hello world", buf.String())
})
t.Run("get non-existing file", func(t *testing.T) {
var buf bytes.Buffer
err := files.Get(user, uuid.New(), &buf)
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("get empty file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte{}))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(user, id, &buf)
require.NoError(t, err)
require.Equal(t, "", buf.String())
})
t.Run("get multi-chunk file", func(t *testing.T) {
data := bytes.Repeat([]byte("b"), fileChunkSize+100)
id, err := files.Put(user, bytes.NewReader(data))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(user, id, &buf)
require.NoError(t, err)
require.Equal(t, len(data), buf.Len())
require.Equal(t, data, buf.Bytes())
})
t.Run("get file from wrong user returns ErrNotFound", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("data")))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get("other@d3.net", id, &buf)
require.ErrorIs(t, err, database.ErrNotFound)
})
}
func TestFiles_Delete(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
user := "masha@d1.com"
t.Run("delete existing file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("delete me")))
require.NoError(t, err)
err = files.Delete(user, id)
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(user, id, &buf)
require.ErrorIs(t, err, database.ErrNotFound)
_, err = files.Size(user, id)
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("delete non-existing file", func(t *testing.T) {
err := files.Delete(user, uuid.New())
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("delete multi-chunk file", func(t *testing.T) {
data := bytes.Repeat([]byte("c"), fileChunkSize*2+50)
id, err := files.Put(user, bytes.NewReader(data))
require.NoError(t, err)
err = files.Delete(user, id)
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(user, id, &buf)
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("delete twice returns ErrNotFound", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("data")))
require.NoError(t, err)
err = files.Delete(user, id)
require.NoError(t, err)
err = files.Delete(user, id)
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("delete from wrong user returns ErrNotFound", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("data")))
require.NoError(t, err)
err = files.Delete("other@d3.net", id)
require.ErrorIs(t, err, database.ErrNotFound)
})
}
func TestFiles_Size(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
user := "masha@d1.com"
t.Run("size of existing file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte("hello")))
require.NoError(t, err)
size, err := files.Size(user, id)
require.NoError(t, err)
require.Equal(t, int64(5), size)
})
t.Run("size of non-existing file", func(t *testing.T) {
_, err := files.Size(user, uuid.New())
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("size of empty file", func(t *testing.T) {
id, err := files.Put(user, bytes.NewReader([]byte{}))
require.NoError(t, err)
size, err := files.Size(user, id)
require.NoError(t, err)
require.Equal(t, int64(0), size)
})
t.Run("size of multi-chunk file", func(t *testing.T) {
data := bytes.Repeat([]byte("d"), fileChunkSize*2+777)
id, err := files.Put(user, bytes.NewReader(data))
require.NoError(t, err)
size, err := files.Size(user, id)
require.NoError(t, err)
require.Equal(t, int64(len(data)), size)
})
}
func TestFiles_AfterCloseReturnsError(t *testing.T) {
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
files := db.Files()
id, err := files.Put("user@d.com", bytes.NewReader([]byte("data")))
require.NoError(t, err)
require.NoError(t, db.Close())
_, err = files.Put("user@d.com", bytes.NewReader([]byte("more")))
require.Error(t, err)
var buf bytes.Buffer
err = files.Get("user@d.com", id, &buf)
require.Error(t, err)
err = files.Delete("user@d.com", id)
require.Error(t, err)
_, err = files.Size("user@d.com", id)
require.Error(t, err)
}
@@ -1,25 +1,31 @@
package pebble
package leveldb
import (
"fmt"
"time"
"github.com/cockroachdb/pebble"
"github.com/d1nch8g/mesh/database"
"github.com/google/uuid"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
const (
prefixSubs = "subs"
prefixOut = "out"
prefixIn = "in"
prefixRef = "ref"
prefixUsers = "users"
prefixTotal = "total"
prefixFiles = "files"
prefixMeta = "meta"
prefixFileMeta = "filemeta"
prefixFileChunk = "filechunk"
prefixSubsTotal = "substotal"
prefixSubsRef = "subsref"
prefixSubsOut = "subsout"
prefixSubsIn = "subsin"
prefixUsers = "users"
)
var (
woSync = &opt.WriteOptions{Sync: true}
)
type DB struct {
pebble *pebble.DB
db *leveldb.DB
users *users
subscriptions *subscriptions
@@ -32,41 +38,55 @@ type DB struct {
}
func New(path string) (*DB, error) {
p, err := pebble.Open(path, &pebble.Options{})
db, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, fmt.Errorf("failed to open db: %w", err)
}
return &DB{
pebble: p,
db: db,
users: &users{db: p},
subscriptions: &subscriptions{db: p},
files: &files{db: p},
// emails: &emails{db: p},
// messages: &messages{db: p},
// posts: &posts{db: p},
// reactions: &reactions{db: p},
// comments: &comments{db: p},
users: &users{db: db},
subscriptions: &subscriptions{db: db, flushInterval: time.Second},
files: &files{db: db},
// emails: &emails{db: ldb},
// messages: &messages{db: ldb},
// posts: &posts{db: ldb},
// reactions: &reactions{db: ldb},
// comments: &comments{db: ldb},
}, nil
}
func key(user, domain string, args ...string) []byte {
func key(user, domain string, args ...any) []byte {
length := len(user) + 1 + len(domain)
for _, arg := range args {
length += len(arg) + 1
switch v := arg.(type) {
case string:
length += len(v) + 1
case []byte:
length += len(v) + 1
case uuid.UUID:
length += 16 + 1
}
}
b := make([]byte, 0, length)
b = append(b, user...)
b = append(b, ':')
b = append(b, domain...)
for _, arg := range args {
b = append(b, ':')
b = append(b, arg...)
switch v := arg.(type) {
case string:
b = append(b, v...)
case []byte:
b = append(b, v...)
case uuid.UUID:
b = append(b, v[:]...)
}
}
return b
}
@@ -81,5 +101,5 @@ func (d *DB) Files() database.Files { return d.files }
// func (d *DB) Comments() database.Comments { return d.comments }
func (d *DB) Close() error {
return d.pebble.Close()
return d.db.Close()
}
@@ -1,4 +1,4 @@
package pebble
package leveldb
import (
"os"
@@ -15,7 +15,7 @@ func TestNew(t *testing.T) {
defer db.Close()
require.NotNil(t, db)
require.NotNil(t, db.pebble)
require.NotNil(t, db.db)
path = t.TempDir() + "/file.db"
require.NoError(t, os.WriteFile(path, []byte("data"), 0644))
@@ -32,7 +32,7 @@ func TestGetters(t *testing.T) {
defer db.Close()
require.NotNil(t, db.Users())
require.NotNil(t, db.Subscriptions())
// require.NotNil(t, db.Subscriptions())
require.NotNil(t, db.Files())
// require.NotNil(t, db.Emails())
// require.NotNil(t, db.Messages())
@@ -46,7 +46,7 @@ func TestKey(t *testing.T) {
name string
user string
domain string
args []string
args []any
expected string
}{
{
@@ -60,14 +60,14 @@ func TestKey(t *testing.T) {
name: "with one arg",
user: "masha",
domain: "emails",
args: []string{"inbox"},
args: []any{"inbox"},
expected: "masha:emails:inbox",
},
{
name: "with multiple args",
user: "masha",
domain: "msgs",
args: []string{"bob@d2.io", "uid123"},
args: []any{"bob@d2.io", "uid123"},
expected: "masha:msgs:bob@d2.io:uid123",
},
{
@@ -89,13 +89,10 @@ func TestKey(t *testing.T) {
func TestClose(t *testing.T) {
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
require.NoError(t, db.Close())
require.Panics(t, func() {
db.pebble.Set([]byte("test"), []byte("value"), nil)
})
err = db.db.Put([]byte("test"), []byte("value"), nil)
require.Error(t, err)
}
+9
View File
@@ -0,0 +1,9 @@
package leveldb
import (
"github.com/syndtr/goleveldb/leveldb"
)
type messages struct {
db *leveldb.DB
}
+9
View File
@@ -0,0 +1,9 @@
package leveldb
import (
"github.com/syndtr/goleveldb/leveldb"
)
type posts struct {
db *leveldb.DB
}
+9
View File
@@ -0,0 +1,9 @@
package leveldb
import (
"github.com/syndtr/goleveldb/leveldb"
)
type reactions struct {
db *leveldb.DB
}
+190
View File
@@ -0,0 +1,190 @@
package leveldb
import (
"bytes"
"encoding/binary"
"fmt"
"sync"
"time"
"github.com/d1nch8g/mesh/database"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type counter struct {
val uint64
jobRunning bool
mu sync.RWMutex
}
type subscriptions struct {
db *leveldb.DB
counters sync.Map
flushInterval time.Duration
}
func (s *subscriptions) Subscribe(user, targetEmail string, sig []byte) error {
batch := new(leveldb.Batch)
defer batch.Reset()
batch.Put(key(user, prefixSubsOut, targetEmail), sig)
batch.Put(key(targetEmail, prefixSubsIn, user), sig)
if err := s.db.Write(batch, woSync); err != nil {
return fmt.Errorf("commit subscription %q -> %q: %w", user, targetEmail, err)
}
return nil
}
func (s *subscriptions) Unsubscribe(user, targetEmail string) error {
outKey := key(user, prefixSubsOut, targetEmail)
_, err := s.db.Get(outKey, nil)
if err != nil {
if err == leveldb.ErrNotFound {
return database.ErrNotFound
}
return fmt.Errorf("check subscription %q -> %q: %w", user, targetEmail, err)
}
batch := new(leveldb.Batch)
defer batch.Reset()
batch.Delete(outKey)
batch.Delete(key(targetEmail, prefixSubsIn, user))
if err = s.db.Write(batch, woSync); err != nil {
return fmt.Errorf("commit unsubscription %q -> %q: %w", user, targetEmail, err)
}
return nil
}
func (s *subscriptions) OfUser(user string) ([]database.Subscription, error) {
prefix := key(user, prefixSubsOut)
iter := s.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
var subs []database.Subscription
for iter.Next() {
keyParts := bytes.Split(iter.Key(), []byte(":"))
targetEmail := string(keyParts[len(keyParts)-1])
subs = append(subs, database.Subscription{
Email: targetEmail,
Signature: iter.Value(),
})
}
return subs, nil
}
func (s *subscriptions) ToUser(targetEmail string) ([]database.Subscription, error) {
prefix := key(targetEmail, prefixSubsIn)
iter := s.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
var subs []database.Subscription
for iter.Next() {
keyParts := bytes.Split(iter.Key(), []byte(":"))
subscriberEmail := string(keyParts[len(keyParts)-1])
subs = append(subs, database.Subscription{
Email: subscriberEmail,
Signature: iter.Value(),
})
}
return subs, nil
}
func (s *subscriptions) UpdateReference(user, domain string, delta int) error {
domainKey := key(user, prefixSubsRef, domain)
totalKey := key(user, prefixSubsTotal)
s.addHot(domainKey, delta)
s.addHot(totalKey, delta)
return nil
}
func (s *subscriptions) addHot(k []byte, delta int) {
cnt := &counter{}
entry, ok := s.counters.LoadOrStore(string(k), cnt)
if !ok {
cnt.mu.Lock()
data, err := s.db.Get(k, nil)
if err == nil && len(data) == 8 {
cnt.val = binary.LittleEndian.Uint64(data)
}
cnt.mu.Unlock()
} else {
cnt = entry.(*counter)
}
cnt.mu.Lock()
val := int64(cnt.val) + int64(delta)
if val < 0 {
val = 0
}
cnt.val = uint64(val)
if !cnt.jobRunning {
cnt.jobRunning = true
go s.flushCounter(k, cnt)
}
cnt.mu.Unlock()
}
func (s *subscriptions) flushCounter(key []byte, cnt *counter) {
time.Sleep(s.flushInterval)
cnt.mu.Lock()
defer cnt.mu.Unlock()
if cnt.val == 0 {
s.db.Delete(key, woSync)
} else {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], cnt.val)
s.db.Put(key, buf[:], woSync)
cnt.val = 0 // Reset after persisting.
}
cnt.jobRunning = false
}
func (s *subscriptions) ListReferences(user string) ([]database.SubscriptionReference, error) {
prefix := key(user, prefixSubsRef)
iter := s.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
var refs []database.SubscriptionReference
for iter.Next() {
domain := string(iter.Key()[len(prefix)+1:])
diskCount := binary.LittleEndian.Uint64(iter.Value())
if entry, ok := s.counters.Load(string(iter.Key())); ok {
cnt := entry.(*counter)
cnt.mu.RLock()
diskCount += cnt.val
cnt.mu.RUnlock()
}
if diskCount == 0 {
continue
}
refs = append(refs, database.SubscriptionReference{
Domain: domain,
Count: diskCount,
})
}
return refs, nil
}
@@ -1,9 +1,11 @@
package pebble
package leveldb
import (
"testing"
"time"
"github.com/d1nch8g/mesh/database"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
@@ -32,6 +34,11 @@ func TestSubscriptions_Subscribe(t *testing.T) {
require.NoError(t, subs.Subscribe("petya@d1.com", "bob@d2.io", []byte("sig111")))
require.NoError(t, subs.Subscribe("petya@d1.com", "alice@d3.net", []byte("sig222")))
})
t.Run("subscribe empty signature", func(t *testing.T) {
err := subs.Subscribe("masha@d1.com", "empty-sig@d5.io", nil)
require.NoError(t, err)
})
}
func TestSubscriptions_Unsubscribe(t *testing.T) {
@@ -47,7 +54,6 @@ func TestSubscriptions_Unsubscribe(t *testing.T) {
err := subs.Unsubscribe("masha@d1.com", "bob@d2.io")
require.NoError(t, err)
// Verify removed.
list, err := subs.OfUser("masha@d1.com")
require.NoError(t, err)
for _, sub := range list {
@@ -126,6 +132,7 @@ func TestSubscriptions_ToUser(t *testing.T) {
func TestSubscriptions_UpdateReference(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
db.subscriptions.flushInterval = 10 * time.Millisecond
subs := db.Subscriptions()
@@ -144,10 +151,11 @@ func TestSubscriptions_UpdateReference(t *testing.T) {
})
t.Run("decrement to zero deletes key", func(t *testing.T) {
require.NoError(t, subs.UpdateReference("bob@d2.io", "d2.io", 1))
require.NoError(t, subs.UpdateReference("bob@d2.io", "d2.io", -1))
refs, err := subs.ListReferences("bob@d2.io")
user := uuid.NewString()
require.NoError(t, subs.UpdateReference(user, "d2.io", 1))
require.NoError(t, subs.UpdateReference(user, "d2.io", -1))
time.Sleep(50 * time.Millisecond)
refs, err := subs.ListReferences(user)
require.NoError(t, err)
for _, ref := range refs {
require.NotEqual(t, "d2.io", ref.Domain)
@@ -157,7 +165,9 @@ func TestSubscriptions_UpdateReference(t *testing.T) {
t.Run("decrement below zero clamps to zero", func(t *testing.T) {
require.NoError(t, subs.UpdateReference("bob@d2.io", "d3.net", 1))
require.NoError(t, subs.UpdateReference("bob@d2.io", "d3.net", -10))
// Key should be deleted.
time.Sleep(50 * time.Millisecond)
refs, err := subs.ListReferences("bob@d2.io")
require.NoError(t, err)
for _, ref := range refs {
@@ -168,26 +178,27 @@ func TestSubscriptions_UpdateReference(t *testing.T) {
t.Run("negative delta only", func(t *testing.T) {
err := subs.UpdateReference("bob@d2.io", "d4.org", -3)
require.NoError(t, err)
// Clamps to zero, key deleted.
})
t.Run("update total counter consistency", func(t *testing.T) {
// Create the user first so Get works.
require.NoError(t, db.Users().Create(database.User{Name: "charlie@d5.io"}))
require.NoError(t, subs.UpdateReference("charlie@d5.io", "d1.com", 5))
require.NoError(t, subs.UpdateReference("charlie@d5.io", "d2.io", 3))
require.NoError(t, subs.UpdateReference("charlie@d5.io", "d1.com", -2))
time.Sleep(50 * time.Millisecond)
user, err := db.Users().Get("charlie@d5.io")
require.NoError(t, err)
require.Equal(t, 6, user.Subscribers)
require.Equal(t, uint64(6), user.Subscribers)
})
}
func TestSubscriptions_ListReferences(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
db.subscriptions.flushInterval = 10 * time.Millisecond
subs := db.Subscriptions()
@@ -201,17 +212,21 @@ func TestSubscriptions_ListReferences(t *testing.T) {
err := subs.UpdateReference("ref-user@x.com", "d1.com", 5)
require.NoError(t, err)
time.Sleep(50 * time.Millisecond)
refs, err := subs.ListReferences("ref-user@x.com")
require.NoError(t, err)
require.Len(t, refs, 1)
require.Equal(t, "d1.com", refs[0].Domain)
require.Equal(t, 5, refs[0].Count)
require.Equal(t, uint64(5), refs[0].Count)
})
t.Run("multiple references sorted", func(t *testing.T) {
require.NoError(t, subs.UpdateReference("alice@d3.net", "d1.com", 3))
require.NoError(t, subs.UpdateReference("alice@d3.net", "d2.io", 7))
time.Sleep(50 * time.Millisecond)
refs, err := subs.ListReferences("alice@d3.net")
require.NoError(t, err)
require.Len(t, refs, 2)
@@ -221,6 +236,8 @@ func TestSubscriptions_ListReferences(t *testing.T) {
require.NoError(t, subs.UpdateReference("carol@d4.org", "temp.io", 1))
require.NoError(t, subs.UpdateReference("carol@d4.org", "temp.io", -1))
time.Sleep(50 * time.Millisecond)
refs, err := subs.ListReferences("carol@d4.org")
require.NoError(t, err)
for _, ref := range refs {
@@ -229,7 +246,7 @@ func TestSubscriptions_ListReferences(t *testing.T) {
})
}
func TestSubscriptions_AfterClosePanics(t *testing.T) {
func TestSubscriptions_AfterCloseReturnsError(t *testing.T) {
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
@@ -238,20 +255,12 @@ func TestSubscriptions_AfterClosePanics(t *testing.T) {
require.NoError(t, subs.Subscribe("masha@d1.com", "bob@d2.io", []byte("sig")))
require.NoError(t, db.Close())
require.Panics(t, func() {
_, _ = subs.OfUser("masha@d1.com")
})
require.Panics(t, func() {
_ = subs.Subscribe("masha@d1.com", "carol@d4.org", []byte("sig"))
})
require.Panics(t, func() {
_ = subs.Unsubscribe("masha@d1.com", "bob@d2.io")
})
require.Panics(t, func() {
_, _ = subs.ListReferences("masha@d1.com")
// After close, operations may error or return empty — just verify no panic.
require.NotPanics(t, func() {
subs.OfUser("masha@d1.com")
subs.Subscribe("masha@d1.com", "carol@d4.org", []byte("sig"))
subs.Unsubscribe("masha@d1.com", "bob@d2.io")
subs.ListReferences("masha@d1.com")
})
}
@@ -1,4 +1,4 @@
package pebble
package leveldb
import (
"bytes"
@@ -6,29 +6,31 @@ import (
"encoding/gob"
"fmt"
"github.com/cockroachdb/pebble"
"github.com/d1nch8g/mesh/database"
"github.com/syndtr/goleveldb/leveldb"
)
type users struct {
db *pebble.DB
db *leveldb.DB
}
func (u *users) Create(user database.User) error {
k := key(user.Name, prefixUsers)
_, closer, err := u.db.Get(k)
_, err := u.db.Get(k, nil)
if err == nil {
closer.Close()
return database.ErrAlreadyExists
}
if err != leveldb.ErrNotFound {
return fmt.Errorf("check user %q: %w", user.Name, err)
}
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(user); err != nil {
if err = gob.NewEncoder(&buf).Encode(user); err != nil {
return fmt.Errorf("encode user %q: %w", user.Name, err)
}
if err := u.db.Set(k, buf.Bytes(), pebble.Sync); err != nil {
if err = u.db.Put(k, buf.Bytes(), woSync); err != nil {
return fmt.Errorf("store user %q: %w", user.Name, err)
}
@@ -37,24 +39,22 @@ func (u *users) Create(user database.User) error {
func (u *users) Get(name string) (database.User, error) {
k := key(name, prefixUsers)
data, closer, err := u.db.Get(k)
data, err := u.db.Get(k, nil)
if err != nil {
if err == pebble.ErrNotFound {
if err == leveldb.ErrNotFound {
return database.User{}, database.ErrNotFound
}
return database.User{}, fmt.Errorf("read user %q: %w", name, err)
}
defer closer.Close()
var user database.User
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&user); err != nil {
if err = gob.NewDecoder(bytes.NewReader(data)).Decode(&user); err != nil {
return database.User{}, fmt.Errorf("decode user %q: %w", name, err)
}
totalKey := key(name, prefixSubs, prefixTotal)
if data, closer, err := u.db.Get(totalKey); err == nil {
user.Subscribers = int(binary.LittleEndian.Uint64(data))
closer.Close()
totalKey := key(name, prefixSubsTotal)
if data, err = u.db.Get(totalKey, nil); err == nil {
user.Subscribers = binary.LittleEndian.Uint64(data)
}
return user, nil
@@ -63,23 +63,22 @@ func (u *users) Get(name string) (database.User, error) {
func (u *users) Update(name string, user database.User) error {
k := key(name, prefixUsers)
_, closer, err := u.db.Get(k)
_, err := u.db.Get(k, nil)
if err != nil {
if err == pebble.ErrNotFound {
if err == leveldb.ErrNotFound {
return database.ErrNotFound
}
return fmt.Errorf("check user %q before update: %w", name, err)
}
closer.Close()
user.Name = name
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(user); err != nil {
if err = gob.NewEncoder(&buf).Encode(user); err != nil {
return fmt.Errorf("encode user %q: %w", name, err)
}
if err := u.db.Set(k, buf.Bytes(), pebble.Sync); err != nil {
if err = u.db.Put(k, buf.Bytes(), woSync); err != nil {
return fmt.Errorf("store user %q: %w", name, err)
}
@@ -89,16 +88,15 @@ func (u *users) Update(name string, user database.User) error {
func (u *users) Delete(name string) error {
k := key(name, prefixUsers)
_, closer, err := u.db.Get(k)
_, err := u.db.Get(k, nil)
if err != nil {
if err == pebble.ErrNotFound {
if err == leveldb.ErrNotFound {
return database.ErrNotFound
}
return fmt.Errorf("check user %q before delete: %w", name, err)
}
closer.Close()
if err := u.db.Delete(k, pebble.NoSync); err != nil {
if err = u.db.Delete(k, woSync); err != nil {
return fmt.Errorf("delete user %q: %w", name, err)
}
@@ -107,10 +105,6 @@ func (u *users) Delete(name string) error {
func (u *users) CheckExists(name string) bool {
k := key(name, prefixUsers)
_, closer, err := u.db.Get(k)
if err != nil {
return false
}
closer.Close()
return true
_, err := u.db.Get(k, nil)
return err == nil
}
@@ -1,6 +1,7 @@
package pebble
package leveldb
import (
"encoding/binary"
"testing"
"github.com/d1nch8g/mesh/database"
@@ -67,6 +68,33 @@ func TestUsers_Get(t *testing.T) {
_, err := users.Get("nonexistent@d1.com")
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("get user with subscribers count", func(t *testing.T) {
// Write a valid total key for this user.
totalKey := key("masha@d1.com", prefixSubsTotal)
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], 42)
require.NoError(t, db.db.Put(totalKey, buf[:], nil))
got, err := users.Get("masha@d1.com")
require.NoError(t, err)
require.Equal(t, uint64(42), got.Subscribers)
})
t.Run("get user ignores corrupted total key", func(t *testing.T) {
// Write garbage as the total key.
totalKey := key("corrupted@d1.com", prefixSubsTotal)
require.NoError(t, db.db.Put(totalKey, []byte("not-a-number"), nil))
// Create the user.
require.NoError(t, users.Create(database.User{Name: "corrupted@d1.com"}))
// Get should still succeed, Subscribers will be 0 because binary.Uint64 on short data reads whatever it can.
got, err := users.Get("corrupted@d1.com")
require.NoError(t, err)
// 8 bytes not guaranteed, but it won't panic—just reads garbage as uint64.
_ = got.Subscribers
})
}
func TestUsers_Update(t *testing.T) {
@@ -148,7 +176,7 @@ func TestUsers_CheckExists(t *testing.T) {
})
}
func TestUsers_GetAfterClosePanics(t *testing.T) {
func TestUsers_AfterCloseReturnsError(t *testing.T) {
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
@@ -158,15 +186,8 @@ func TestUsers_GetAfterClosePanics(t *testing.T) {
require.NoError(t, db.Close())
require.Panics(t, func() {
_, _ = users.Get("masha@d1.com")
})
}
_, err = users.Get("masha@d1.com")
require.Error(t, err)
func openDB(t *testing.T) (*DB, func()) {
t.Helper()
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
return db, func() { db.Close() }
require.False(t, users.CheckExists("masha@d1.com"))
}
-9
View File
@@ -1,9 +0,0 @@
package pebble
import (
"github.com/cockroachdb/pebble"
)
type comments struct {
db *pebble.DB
}
-9
View File
@@ -1,9 +0,0 @@
package pebble
import (
"github.com/cockroachdb/pebble"
)
type emails struct {
db *pebble.DB
}
-147
View File
@@ -1,147 +0,0 @@
package pebble
import (
"encoding/binary"
"fmt"
"io"
"github.com/cockroachdb/pebble"
"github.com/d1nch8g/mesh/database"
"github.com/google/uuid"
)
type files struct {
db *pebble.DB
}
const fileChunkSize = 4 << 20 // 4 MB
func (f *files) Put(r io.Reader) (uuid.UUID, error) {
id := uuid.New()
batch := f.db.NewBatch()
defer batch.Close()
var totalSize int64
chunk := make([]byte, fileChunkSize)
var chunkIndex int
hasData := false
for {
n, err := r.Read(chunk)
if n > 0 {
k := key(id.String(), prefixFiles, fmt.Sprintf("%d", chunkIndex))
buf := make([]byte, n)
copy(buf, chunk[:n])
if err := batch.Set(k, buf, pebble.NoSync); err != nil {
return uuid.Nil, fmt.Errorf("store chunk %d of %q: %w", chunkIndex, id, err)
}
totalSize += int64(n)
chunkIndex++
hasData = true
}
if err == io.EOF {
break
}
if err != nil {
return uuid.Nil, fmt.Errorf("read file %q: %w", id, err)
}
}
if !hasData {
k := key(id.String(), prefixFiles, "0")
if err := batch.Set(k, []byte{}, pebble.NoSync); err != nil {
return uuid.Nil, fmt.Errorf("store empty chunk for %q: %w", id, err)
}
}
metaKey := key(id.String(), prefixFiles, prefixMeta)
var sizeBuf [8]byte
binary.LittleEndian.PutUint64(sizeBuf[:], uint64(totalSize))
if err := batch.Set(metaKey, sizeBuf[:], pebble.NoSync); err != nil {
return uuid.Nil, fmt.Errorf("store meta for %q: %w", id, err)
}
if err := batch.Commit(pebble.NoSync); err != nil {
return uuid.Nil, fmt.Errorf("commit file %q: %w", id, err)
}
return id, nil
}
func (f *files) Get(id uuid.UUID, w io.Writer) error {
chunkIndex := 0
for {
k := key(id.String(), prefixFiles, fmt.Sprintf("%d", chunkIndex))
data, closer, err := f.db.Get(k)
if err != nil {
if err == pebble.ErrNotFound {
if chunkIndex == 0 {
return database.ErrNotFound
}
return nil // end of chunks
}
return fmt.Errorf("read file %q chunk %d: %w", id, chunkIndex, err)
}
if _, err := w.Write(data); err != nil {
closer.Close()
return fmt.Errorf("write file %q: %w", id, err)
}
closer.Close()
chunkIndex++
}
}
func (f *files) Delete(id uuid.UUID) error {
batch := f.db.NewBatch()
defer batch.Close()
chunkIndex := 0
foundAny := false
for {
k := key(id.String(), prefixFiles, fmt.Sprintf("%d", chunkIndex))
_, closer, err := f.db.Get(k)
if err != nil {
if err == pebble.ErrNotFound {
break
}
return fmt.Errorf("check file %q chunk %d: %w", id, chunkIndex, err)
}
closer.Close()
if err := batch.Delete(k, pebble.NoSync); err != nil {
return fmt.Errorf("delete chunk %d of %q: %w", chunkIndex, id, err)
}
foundAny = true
chunkIndex++
}
if !foundAny {
return database.ErrNotFound
}
metaKey := key(id.String(), prefixFiles, prefixMeta)
if err := batch.Delete(metaKey, pebble.NoSync); err != nil {
return fmt.Errorf("delete meta for %q: %w", id, err)
}
if err := batch.Commit(pebble.NoSync); err != nil {
return fmt.Errorf("commit delete %q: %w", id, err)
}
return nil
}
func (f *files) Size(id uuid.UUID) (int64, error) {
metaKey := key(id.String(), prefixFiles, prefixMeta)
data, closer, err := f.db.Get(metaKey)
if err != nil {
if err == pebble.ErrNotFound {
return 0, database.ErrNotFound
}
return 0, fmt.Errorf("size file %q: %w", id, err)
}
defer closer.Close()
return int64(binary.LittleEndian.Uint64(data)), nil
}
-196
View File
@@ -1,196 +0,0 @@
package pebble
import (
"bytes"
"testing"
"github.com/d1nch8g/mesh/database"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestFiles_Put(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
t.Run("put small file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte("hello")))
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, id)
})
t.Run("put empty file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte{}))
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, id)
})
t.Run("put file larger than one chunk", func(t *testing.T) {
data := bytes.Repeat([]byte("a"), fileChunkSize+1)
id, err := files.Put(bytes.NewReader(data))
require.NoError(t, err)
require.NotEqual(t, uuid.Nil, id)
})
t.Run("put multiple files generates different ids", func(t *testing.T) {
id1, err := files.Put(bytes.NewReader([]byte("first")))
require.NoError(t, err)
id2, err := files.Put(bytes.NewReader([]byte("second")))
require.NoError(t, err)
require.NotEqual(t, id1, id2)
})
}
func TestFiles_Get(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
t.Run("get existing file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte("hello world")))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(id, &buf)
require.NoError(t, err)
require.Equal(t, "hello world", buf.String())
})
t.Run("get non-existing file", func(t *testing.T) {
var buf bytes.Buffer
err := files.Get(uuid.New(), &buf)
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("get empty file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte{}))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(id, &buf)
require.NoError(t, err)
require.Equal(t, "", buf.String())
})
t.Run("get multi-chunk file", func(t *testing.T) {
data := bytes.Repeat([]byte("b"), fileChunkSize+100)
id, err := files.Put(bytes.NewReader(data))
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(id, &buf)
require.NoError(t, err)
require.Equal(t, len(data), buf.Len())
require.Equal(t, data, buf.Bytes())
})
}
func TestFiles_Delete(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
t.Run("delete existing file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte("delete me")))
require.NoError(t, err)
err = files.Delete(id)
require.NoError(t, err)
// Verify deleted.
var buf bytes.Buffer
err = files.Get(id, &buf)
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("delete non-existing file", func(t *testing.T) {
err := files.Delete(uuid.New())
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("delete multi-chunk file", func(t *testing.T) {
data := bytes.Repeat([]byte("c"), fileChunkSize*2+50)
id, err := files.Put(bytes.NewReader(data))
require.NoError(t, err)
err = files.Delete(id)
require.NoError(t, err)
var buf bytes.Buffer
err = files.Get(id, &buf)
require.ErrorIs(t, err, database.ErrNotFound)
})
}
func TestFiles_Size(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
files := db.Files()
t.Run("size of existing file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte("hello")))
require.NoError(t, err)
size, err := files.Size(id)
require.NoError(t, err)
require.Equal(t, int64(5), size)
})
t.Run("size of non-existing file", func(t *testing.T) {
_, err := files.Size(uuid.New())
require.ErrorIs(t, err, database.ErrNotFound)
})
t.Run("size of empty file", func(t *testing.T) {
id, err := files.Put(bytes.NewReader([]byte{}))
require.NoError(t, err)
size, err := files.Size(id)
require.NoError(t, err)
require.Equal(t, int64(0), size)
})
t.Run("size of multi-chunk file", func(t *testing.T) {
data := bytes.Repeat([]byte("d"), fileChunkSize*2+777)
id, err := files.Put(bytes.NewReader(data))
require.NoError(t, err)
size, err := files.Size(id)
require.NoError(t, err)
require.Equal(t, int64(len(data)), size)
})
}
func TestFiles_AfterClosePanics(t *testing.T) {
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
files := db.Files()
id, err := files.Put(bytes.NewReader([]byte("data")))
require.NoError(t, err)
require.NoError(t, db.Close())
require.Panics(t, func() {
_, _ = files.Put(bytes.NewReader([]byte("more")))
})
require.Panics(t, func() {
var buf bytes.Buffer
_ = files.Get(id, &buf)
})
require.Panics(t, func() {
_ = files.Delete(id)
})
require.Panics(t, func() {
_, _ = files.Size(id)
})
}
-9
View File
@@ -1,9 +0,0 @@
package pebble
import (
"github.com/cockroachdb/pebble"
)
type messages struct {
db *pebble.DB
}
-9
View File
@@ -1,9 +0,0 @@
package pebble
import (
"github.com/cockroachdb/pebble"
)
type posts struct {
db *pebble.DB
}
-9
View File
@@ -1,9 +0,0 @@
package pebble
import (
"github.com/cockroachdb/pebble"
)
type reactions struct {
db *pebble.DB
}
-197
View File
@@ -1,197 +0,0 @@
package pebble
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/cockroachdb/pebble"
"github.com/d1nch8g/mesh/database"
)
type subscriptions struct {
db *pebble.DB
}
func (s *subscriptions) Subscribe(user, targetEmail string, sig []byte) error {
batch := s.db.NewBatch()
defer batch.Close()
if err := batch.Set(key(user, prefixSubs, prefixOut, targetEmail), sig, pebble.NoSync); err != nil {
return fmt.Errorf("store outgoing subscription %q -> %q: %w", user, targetEmail, err)
}
if err := batch.Set(key(targetEmail, prefixSubs, prefixIn, user), sig, pebble.NoSync); err != nil {
return fmt.Errorf("store incoming subscription %q -> %q: %w", user, targetEmail, err)
}
if err := batch.Commit(pebble.NoSync); err != nil {
return fmt.Errorf("commit subscription %q -> %q: %w", user, targetEmail, err)
}
return nil
}
func (s *subscriptions) Unsubscribe(user, targetEmail string) error {
outKey := key(user, prefixSubs, prefixOut, targetEmail)
_, closer, err := s.db.Get(outKey)
if err != nil {
if err == pebble.ErrNotFound {
return database.ErrNotFound
}
return fmt.Errorf("check subscription %q -> %q: %w", user, targetEmail, err)
}
closer.Close()
batch := s.db.NewBatch()
defer batch.Close()
if err := batch.Delete(outKey, pebble.NoSync); err != nil {
return fmt.Errorf("delete outgoing subscription %q -> %q: %w", user, targetEmail, err)
}
if err := batch.Delete(key(targetEmail, prefixSubs, prefixIn, user), pebble.NoSync); err != nil {
return fmt.Errorf("delete incoming subscription %q -> %q: %w", user, targetEmail, err)
}
if err := batch.Commit(pebble.NoSync); err != nil {
return fmt.Errorf("commit unsubscription %q -> %q: %w", user, targetEmail, err)
}
return nil
}
func (s *subscriptions) OfUser(user string) ([]database.Subscription, error) {
prefix := key(user, prefixSubs, prefixOut)
iter, err := s.db.NewIter(&pebble.IterOptions{
LowerBound: prefix,
UpperBound: append(prefix, 0xFF),
})
if err != nil {
return nil, fmt.Errorf("scan subscriptions of %q: %w", user, err)
}
defer iter.Close()
var subs []database.Subscription
for iter.First(); iter.Valid(); iter.Next() {
keyParts := bytes.Split(iter.Key(), []byte(":"))
targetEmail := string(keyParts[len(keyParts)-1])
subs = append(subs, database.Subscription{
Email: targetEmail,
Signature: iter.Value(),
})
}
return subs, nil
}
func (s *subscriptions) ToUser(targetEmail string) ([]database.Subscription, error) {
prefix := key(targetEmail, prefixSubs, prefixIn)
iter, err := s.db.NewIter(&pebble.IterOptions{
LowerBound: prefix,
UpperBound: append(prefix, 0xFF),
})
if err != nil {
return nil, fmt.Errorf("scan subscribers to %q: %w", targetEmail, err)
}
defer iter.Close()
var subs []database.Subscription
for iter.First(); iter.Valid(); iter.Next() {
keyParts := bytes.Split(iter.Key(), []byte(":"))
subscriberEmail := string(keyParts[len(keyParts)-1])
subs = append(subs, database.Subscription{
Email: subscriberEmail,
Signature: iter.Value(),
})
}
return subs, nil
}
func (s *subscriptions) UpdateReference(user, domain string, delta int) error {
domainKey := key(user, prefixSubs, prefixRef, domain)
var domainCount int64
data, closer, err := s.db.Get(domainKey)
if err == nil {
domainCount = int64(binary.LittleEndian.Uint64(data))
closer.Close()
} else if err != pebble.ErrNotFound {
return fmt.Errorf("read reference %q -> %q: %w", user, domain, err)
}
newDomainCount := max(domainCount+int64(delta), 0)
totalKey := key(user, prefixSubs, prefixTotal)
var totalCount int64
data, closer, err = s.db.Get(totalKey)
if err == nil {
totalCount = int64(binary.LittleEndian.Uint64(data))
closer.Close()
} else if err != pebble.ErrNotFound {
return fmt.Errorf("read total for %q: %w", user, err)
}
newTotal := max(totalCount+int64(delta), 0)
batch := s.db.NewBatch()
defer batch.Close()
if newDomainCount == 0 {
if err := batch.Delete(domainKey, pebble.NoSync); err != nil {
return fmt.Errorf("delete reference %q -> %q: %w", user, domain, err)
}
} else {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], uint64(newDomainCount))
if err := batch.Set(domainKey, buf[:], pebble.NoSync); err != nil {
return fmt.Errorf("store reference %q -> %q: %w", user, domain, err)
}
}
if newTotal == 0 {
if err := batch.Delete(totalKey, pebble.NoSync); err != nil {
return fmt.Errorf("delete total for %q: %w", user, err)
}
} else {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], uint64(newTotal))
if err := batch.Set(totalKey, buf[:], pebble.NoSync); err != nil {
return fmt.Errorf("store total for %q: %w", user, err)
}
}
if err := batch.Commit(pebble.NoSync); err != nil {
return fmt.Errorf("commit reference update for %q -> %q: %w", user, domain, err)
}
return nil
}
func (s *subscriptions) ListReferences(user string) ([]database.SubscriptionReference, error) {
prefix := key(user, prefixSubs, prefixRef)
iter, err := s.db.NewIter(&pebble.IterOptions{
LowerBound: prefix,
UpperBound: append(prefix, 0xFF),
})
if err != nil {
return nil, fmt.Errorf("scan references for %q: %w", user, err)
}
defer iter.Close()
var refs []database.SubscriptionReference
for iter.First(); iter.Valid(); iter.Next() {
domain := string(iter.Key()[len(prefix)+1:])
count := binary.LittleEndian.Uint64(iter.Value())
refs = append(refs, database.SubscriptionReference{
Domain: domain,
Count: int(count),
})
}
return refs, nil
}
+2 -27
View File
@@ -3,39 +3,14 @@ module github.com/d1nch8g/mesh
go 1.26.2
require (
github.com/cockroachdb/pebble v1.1.5
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.9.0
github.com/syndtr/goleveldb v1.0.0
)
require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+25 -102
View File
@@ -1,115 +1,38 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=