Skip to content

Commit 9caec09

Browse files
feat(GODT-1570): Badger store implementation
Add implementation of Badger (https://github.com/dgraph-io/badger) as an alternative cache implementation. This will become useful later as part of GODT-1642 where we need to ensure that cache modifications can be rolled back with failed db modifications and vice-versa. The unit tests have been updated to use this new implementation and it is also available to gluon-bench using the `-store=badger` option. Finally the `StoreBuilder` has been updated to accept a slice of bytes rather than a string as the encryption key.
1 parent b57cfc9 commit 9caec09

13 files changed

Lines changed: 363 additions & 9 deletions

File tree

benchmarks/gluon_bench/gluon_benchmarks/sync.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gluon_benchmarks
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"flag"
67
"math/rand"
78
"time"
@@ -99,10 +100,12 @@ func (s *Sync) setupConnector(ctx context.Context) (utils.ConnectorImpl, error)
99100
s.mailboxes = append(s.mailboxes, mboxID)
100101
}
101102

103+
encryptionBytes := sha256.Sum256([]byte(*flags.UserPassword))
104+
102105
if _, err = s.server.AddUser(
103106
ctx,
104107
c.Connector(),
105-
*flags.UserPassword); err != nil {
108+
encryptionBytes[:]); err != nil {
106109
return nil, err
107110
}
108111

benchmarks/gluon_bench/imap_benchmarks/server/local.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"fmt"
67
"net"
78

@@ -72,10 +73,12 @@ func addUser(ctx context.Context, server *gluon.Server) error {
7273
return nil
7374
}
7475

76+
encryptionBytes := sha256.Sum256([]byte(*flags.UserPassword))
77+
7578
if userID, err := server.AddUser(
7679
ctx,
7780
c.Connector(),
78-
*flags.UserPassword); err != nil {
81+
encryptionBytes[:]); err != nil {
7982
return err
8083
} else if *flags.Verbose {
8184
fmt.Printf("Adding user ID=%v\n", userID)

benchmarks/gluon_bench/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
_ "github.com/ProtonMail/gluon/benchmarks/gluon_bench/gluon_benchmarks"
66
_ "github.com/ProtonMail/gluon/benchmarks/gluon_bench/imap_benchmarks"
77
_ "github.com/ProtonMail/gluon/benchmarks/gluon_bench/store_benchmarks"
8+
"github.com/sirupsen/logrus"
89
)
910

1011
func main() {
12+
logrus.SetLevel(logrus.ErrorLevel)
1113
benchmark.RunMain()
1214
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package store_benchmarks
2+
3+
import (
4+
"crypto/sha256"
5+
6+
"github.com/ProtonMail/gluon/benchmarks/gluon_bench/flags"
7+
"github.com/ProtonMail/gluon/store"
8+
"github.com/google/uuid"
9+
)
10+
11+
type BadgerStoreBuilder struct{}
12+
13+
func (*BadgerStoreBuilder) New(path string) (store.Store, error) {
14+
encryptionKey := sha256.Sum256([]byte(*flags.UserPassword))
15+
return store.NewBadgerStore(path, uuid.NewString(), encryptionKey[:])
16+
}
17+
18+
func init() {
19+
RegisterStoreBuilder("badger", &BadgerStoreBuilder{})
20+
}

demo/demo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func addUser(ctx context.Context, server *gluon.Server, addresses []string, pass
9191
userID, err := server.AddUser(
9292
ctx,
9393
connector,
94-
password,
94+
[]byte(password),
9595
)
9696
if err != nil {
9797
return err

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
entgo.io/ent v0.10.1
77
github.com/ProtonMail/gopenpgp/v2 v2.4.7
88
github.com/bradenaw/juniper v0.6.0
9+
github.com/dgraph-io/badger/v3 v3.2103.2
910
github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317
1011
github.com/emersion/go-imap-uidplus v0.0.0-20200503180755-e75854c361e9
1112
github.com/emersion/go-mbox v1.0.2
@@ -27,19 +28,32 @@ require (
2728
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f // indirect
2829
github.com/agext/levenshtein v1.2.1 // indirect
2930
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
31+
github.com/cespare/xxhash v1.1.0 // indirect
32+
github.com/cespare/xxhash/v2 v2.1.1 // indirect
3033
github.com/davecgh/go-spew v1.1.1 // indirect
34+
github.com/dgraph-io/ristretto v0.1.0 // indirect
35+
github.com/dustin/go-humanize v1.0.0 // indirect
3136
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
3237
github.com/go-openapi/inflect v0.19.0 // indirect
38+
github.com/gogo/protobuf v1.3.2 // indirect
39+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
40+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
41+
github.com/golang/protobuf v1.5.0 // indirect
42+
github.com/golang/snappy v0.0.3 // indirect
43+
github.com/google/flatbuffers v1.12.1 // indirect
3344
github.com/google/go-cmp v0.5.6 // indirect
3445
github.com/hashicorp/hcl/v2 v2.10.0 // indirect
46+
github.com/klauspost/compress v1.12.3 // indirect
3547
github.com/kr/pretty v0.3.0 // indirect
3648
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
3749
github.com/pkg/errors v0.9.1 // indirect
3850
github.com/pmezard/go-difflib v1.0.0 // indirect
3951
github.com/rogpeppe/go-internal v1.8.0 // indirect
4052
github.com/zclconf/go-cty v1.8.0 // indirect
53+
go.opencensus.io v0.23.0 // indirect
4154
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
4255
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
56+
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
4357
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e // indirect
4458
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
4559
)

go.sum

Lines changed: 141 additions & 0 deletions
Large diffs are not rendered by default.

internal/backend/user.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ func (user *user) close(ctx context.Context) error {
114114
// Wait until the connector update go routine has finished.
115115
user.updateWG.Wait()
116116

117+
if err := user.store.Close(); err != nil {
118+
return fmt.Errorf("failed to close user client storage: %w", err)
119+
}
120+
117121
if err := user.client.Close(); err != nil {
118122
return fmt.Errorf("failed to close user client: %w", err)
119123
}

server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func getDatabasePath(userPath, userID string) string {
9696

9797
// AddUser creates a new user and generates new unique ID for this user. If you have an existing userID, please use
9898
// LoadUser instead.
99-
func (s *Server) AddUser(ctx context.Context, conn connector.Connector, encryptionPassphrase string) (string, error) {
99+
func (s *Server) AddUser(ctx context.Context, conn connector.Connector, encryptionPassphrase []byte) (string, error) {
100100
userID := s.backend.NewUserID()
101101

102102
if err := s.LoadUser(ctx, conn, userID, encryptionPassphrase); err != nil {
@@ -108,7 +108,7 @@ func (s *Server) AddUser(ctx context.Context, conn connector.Connector, encrypti
108108

109109
// LoadUser loads an existing user's data from disk. This function can also be used to assign a custom userID to a mail
110110
// server user.
111-
func (s *Server) LoadUser(ctx context.Context, conn connector.Connector, userID, encryptionPassphrase string) error {
111+
func (s *Server) LoadUser(ctx context.Context, conn connector.Connector, userID string, encryptionPassphrase []byte) error {
112112
userPath, err := s.GetUserDataPath(userID)
113113
if err != nil {
114114
return err

store/badger.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package store
2+
3+
import (
4+
"path/filepath"
5+
"sync"
6+
"time"
7+
8+
"github.com/dgraph-io/badger/v3"
9+
"github.com/sirupsen/logrus"
10+
)
11+
12+
type BadgerStore struct {
13+
db *badger.DB
14+
gcExitCh chan struct{}
15+
wg sync.WaitGroup
16+
}
17+
18+
func logrusLevelToBadgerLevel(options badger.Options) badger.Options {
19+
switch logrus.GetLevel() {
20+
case logrus.InfoLevel:
21+
return options.WithLoggingLevel(badger.INFO)
22+
case logrus.TraceLevel:
23+
fallthrough
24+
case logrus.DebugLevel:
25+
return options.WithLoggingLevel(badger.DEBUG)
26+
case logrus.WarnLevel:
27+
return options.WithLoggingLevel(badger.WARNING)
28+
case logrus.FatalLevel:
29+
fallthrough
30+
case logrus.PanicLevel:
31+
fallthrough
32+
case logrus.ErrorLevel:
33+
return options.WithLoggingLevel(badger.ERROR)
34+
default:
35+
return options.WithLoggingLevel(badger.ERROR)
36+
}
37+
}
38+
39+
func NewBadgerStore(path string, userID string, encryptionPassphrase []byte) (*BadgerStore, error) {
40+
directory := filepath.Join(path, userID)
41+
db, err := badger.Open(logrusLevelToBadgerLevel(badger.DefaultOptions(directory)).
42+
WithLogger(logrus.StandardLogger()).
43+
WithEncryptionKey(encryptionPassphrase).
44+
WithIndexCacheSize(128 * 1024 * 1024))
45+
46+
if err != nil {
47+
return nil, nil
48+
}
49+
50+
store := &BadgerStore{db: db, gcExitCh: make(chan struct{})}
51+
52+
store.startGCCollector()
53+
54+
return store, nil
55+
}
56+
57+
func (b *BadgerStore) startGCCollector() {
58+
// Garbage collection needs to be run manually by us at some point.
59+
// See https://dgraph.io/docs/badger/get-started/#garbage-collection for more details.
60+
b.wg.Add(1)
61+
62+
go func() {
63+
defer b.wg.Done()
64+
65+
gcRun := time.After(5 * time.Minute)
66+
67+
select {
68+
case <-gcRun:
69+
{
70+
again:
71+
err := b.db.RunValueLogGC(0.7)
72+
if err == nil {
73+
goto again
74+
}
75+
}
76+
case <-b.gcExitCh:
77+
return
78+
}
79+
}()
80+
}
81+
82+
func (b *BadgerStore) Get(messageID string) ([]byte, error) {
83+
var data []byte
84+
85+
if err := b.db.View(func(txn *badger.Txn) error {
86+
item, err := txn.Get([]byte(messageID))
87+
if err != nil {
88+
return err
89+
}
90+
91+
data, err = item.ValueCopy(nil)
92+
if err != nil {
93+
return err
94+
}
95+
96+
return nil
97+
}); err != nil {
98+
return nil, err
99+
}
100+
101+
return data, nil
102+
}
103+
104+
func (b *BadgerStore) Set(messageID string, literal []byte) error {
105+
return b.db.Update(func(txn *badger.Txn) error {
106+
return txn.Set([]byte(messageID), literal)
107+
})
108+
}
109+
110+
func (b *BadgerStore) Update(oldID, newID string) error {
111+
return b.db.Update(func(txn *badger.Txn) error {
112+
oldIDBytes := []byte(oldID)
113+
newIDBytes := []byte(newID)
114+
115+
item, err := txn.Get(oldIDBytes)
116+
if err != nil {
117+
return err
118+
}
119+
120+
buffer := make([]byte, item.ValueSize())
121+
buffer, err = item.ValueCopy(buffer)
122+
if err != nil {
123+
return err
124+
}
125+
126+
if err := txn.Set(newIDBytes, buffer); err != nil {
127+
return err
128+
}
129+
130+
return txn.Delete(oldIDBytes)
131+
})
132+
}
133+
134+
func (b *BadgerStore) Delete(messageID ...string) error {
135+
return b.db.Update(func(txn *badger.Txn) error {
136+
for _, v := range messageID {
137+
if err := txn.Delete([]byte(v)); err != nil {
138+
return err
139+
}
140+
}
141+
142+
return nil
143+
})
144+
}
145+
146+
func (b *BadgerStore) Close() error {
147+
close(b.gcExitCh)
148+
b.wg.Wait()
149+
150+
return b.db.Close()
151+
}
152+
153+
type BadgerStoreBuilder struct{}
154+
155+
func (*BadgerStoreBuilder) New(directory, userID, encryptionPassphrase string) (Store, error) {
156+
return NewBadgerStore(directory, userID, []byte(encryptionPassphrase))
157+
}

0 commit comments

Comments
 (0)