-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace dgraph-io/badger cache storage with etcd-io/bbolt #42571
Open
stefans-elastic
wants to merge
36
commits into
elastic:main
Choose a base branch
from
stefans-elastic:drop-dbadger-io
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+333
−597
Open
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
4f10346
add bbolt store for key-value cache
stefans-elastic fbe7ebf
fix unit test failures
stefans-elastic 7fa4f37
delete expired keys
stefans-elastic 1c4a798
set expireAt properly
stefans-elastic 183b395
add unit tests
stefans-elastic 36c3933
add missing license headers
stefans-elastic 9b20f2d
go mod tidy, update NOTICE.txt
stefans-elastic f2303dd
fix linter issues
stefans-elastic 19bed5b
add more godoc comments
stefans-elastic 17372b1
Merge branch 'main' of github.com:stefans-elastic/beats into drop-dba…
stefans-elastic 7ab7864
add CHANGELOG-developer.next.asciidoc entry
stefans-elastic 91a3ac2
rename Ttl to TTL to comply with styleguide
stefans-elastic 6ea76db
simplify Set method transaction code
stefans-elastic 21e1b2f
Get/Set transaction refactoring
stefans-elastic ae592ab
adress PR comments
stefans-elastic 6041c9b
Update x-pack/libbeat/kv/bbolt/bbolt.go
stefans-elastic aec3e8e
rename Kv -> KV
stefans-elastic 26e8277
Merge branch 'drop-dbadger-io' of github.com:stefans-elastic/beats in…
stefans-elastic e474690
Merge branch 'main' of github.com:stefans-elastic/beats into drop-dba…
stefans-elastic 8675a71
replace assert.NoError with require.NoError in unit test
stefans-elastic d606724
don't store TTL as part of KV value in bbolt store
stefans-elastic 2cc0ccd
rename Connect -> Open
stefans-elastic 2b9137d
Merge branch 'main' into drop-dbadger-io
stefans-elastic 0614e03
add debug log upon TTL refreshing (if refreshOnAccess is true)
stefans-elastic 94d8f6d
Merge branch 'drop-dbadger-io' of github.com:stefans-elastic/beats in…
stefans-elastic 5f22d0f
update godoc comment for x-pack/libbeat/persistentcache/persistentcac…
stefans-elastic dcd7bc7
PR comment resolved
stefans-elastic fa9f41c
rename boltKvStore -> bboltkv on import renaming to make it more idio…
stefans-elastic 43d2f8f
include error in log message
stefans-elastic 041ddb8
Merge branch 'main' into drop-dbadger-io
stefans-elastic 477fb0a
update CHANGELOG-developer.next.asciidoc message
stefans-elastic 3d37518
Merge branch 'main' into drop-dbadger-io
stefans-elastic c77b7f1
add CHANGELOG.next.asciidoc entry
stefans-elastic ddc31ed
Merge branch 'main' into drop-dbadger-io
stefans-elastic c47826e
Update CHANGELOG.next.asciidoc
stefans-elastic 30a92e9
move entry to correct section in CHANGELOG.next.asciidoc
stefans-elastic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
// or more contributor license agreements. Licensed under the Elastic License; | ||
// you may not use this file except in compliance with the Elastic License. | ||
|
||
package bbolt | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"path" | ||
"time" | ||
|
||
bolt "go.etcd.io/bbolt" | ||
) | ||
|
||
const ( | ||
defaultDbPath = "beat_cache.db" | ||
defaultBucketName = "kv" | ||
defaultDbFileMode = 0o600 | ||
) | ||
|
||
// BboltValue - value type used for storage in bolt DB. | ||
type BboltValue struct { | ||
RawValue []byte `json:"rawValue"` | ||
|
||
// ExpireAt - Unix timestamp (in nanoseconds) of time when value expires. | ||
ExpireAt int64 `json:"expireAt"` | ||
} | ||
|
||
type Option func(bbolt *Bbolt) | ||
|
||
type Bbolt struct { | ||
dbPath string | ||
dbFileMode os.FileMode | ||
bucketName string | ||
|
||
db *bolt.DB | ||
} | ||
|
||
// New creates and returns instance of bolt key-value cache implementation | ||
func New(options ...Option) *Bbolt { | ||
b := &Bbolt{ | ||
dbPath: defaultDbPath, | ||
dbFileMode: defaultDbFileMode, | ||
bucketName: defaultBucketName, | ||
} | ||
for _, opt := range options { | ||
opt(b) | ||
} | ||
|
||
return b | ||
} | ||
|
||
func WithDbPath(path string) Option { | ||
return func(b *Bbolt) { | ||
b.dbPath = path | ||
} | ||
} | ||
|
||
func WithDbFileMode(mode os.FileMode) Option { | ||
return func(b *Bbolt) { | ||
b.dbFileMode = mode | ||
} | ||
} | ||
|
||
func WithBucketName(name string) Option { | ||
return func(b *Bbolt) { | ||
b.bucketName = name | ||
} | ||
} | ||
|
||
// Open creates directories of a given path for bbolt DB file (if directories not already exist), creates DB file with given file permissions, creates bucket to store cache data. | ||
func (b *Bbolt) Open() error { | ||
var err error | ||
|
||
dbDir := path.Dir(b.dbPath) | ||
err = os.MkdirAll(dbDir, b.dbFileMode) | ||
if err != nil { | ||
return fmt.Errorf("bbolt: creation of the directory for DB failed: %w", err) | ||
} | ||
|
||
b.db, err = openDbFile(b.dbPath, b.dbFileMode) | ||
if err != nil { | ||
return fmt.Errorf("bbolt: openDbFile error: %w", err) | ||
} | ||
err = b.ensureBucketExists() | ||
if err != nil { | ||
return fmt.Errorf("bbolt: bucket opening error: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
// Get fetches value by key from bolt DB (returns nil if key is not present or expired) | ||
func (b *Bbolt) Get(key []byte) (data []byte, err error) { | ||
// we need writable transaction here in order to delete expired keys | ||
err = b.db.Update(func(tx *bolt.Tx) error { | ||
bucket := tx.Bucket([]byte(b.bucketName)) | ||
|
||
jsonVal := bucket.Get(key) | ||
if jsonVal == nil { // no value in store | ||
return nil | ||
} | ||
|
||
var val BboltValue | ||
if err := json.Unmarshal(jsonVal, &val); err != nil { | ||
return err | ||
} | ||
if val.ExpireAt != 0 && val.ExpireAt <= time.Now().UnixNano() { // value expired | ||
return bucket.Delete(key) | ||
} | ||
data = val.RawValue | ||
return nil | ||
}) | ||
return data, err | ||
} | ||
|
||
// Set stores a key-value pair in the database. If TTL is 0, the key does not expire. | ||
func (b *Bbolt) Set(key []byte, value []byte, ttl time.Duration) error { | ||
return b.db.Update(func(tx *bolt.Tx) error { | ||
bucket := tx.Bucket([]byte(b.bucketName)) | ||
|
||
bboltValEncoded, err := getMarshalledBboltValue(value, ttl) | ||
if err != nil { | ||
return err | ||
} | ||
err = bucket.Put(key, bboltValEncoded) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
}) | ||
} | ||
|
||
// Close closes the database. | ||
func (b *Bbolt) Close() error { | ||
return b.db.Close() | ||
} | ||
|
||
// ensureBucketExists - creates bolt bucket if it doesn't already exist. | ||
func (b *Bbolt) ensureBucketExists() error { | ||
err := b.db.Update(func(tx *bolt.Tx) error { | ||
_, err := tx.CreateBucketIfNotExists([]byte(b.bucketName)) | ||
return err | ||
}) | ||
return err | ||
} | ||
|
||
// openDbFile opens bolt DB file and returns *bolt.DB instance | ||
func openDbFile(path string, mode os.FileMode) (*bolt.DB, error) { | ||
db, err := bolt.Open(path, mode, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return db, nil | ||
} | ||
|
||
// getMarshalledBboltValue returns json encoded BboltValue constructed from raw value and TTL. | ||
func getMarshalledBboltValue(value []byte, ttl time.Duration) ([]byte, error) { | ||
return json.Marshal(newBboltValue(value, ttl)) | ||
} | ||
|
||
// newBboltValue creates BboltValue from raw value and TTL | ||
func newBboltValue(value []byte, ttl time.Duration) BboltValue { | ||
var expireAt int64 | ||
if ttl > 0 { | ||
expireAt = time.Now().Add(ttl).UnixNano() | ||
} | ||
return BboltValue{ | ||
RawValue: value, | ||
ExpireAt: expireAt, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
// or more contributor license agreements. Licensed under the Elastic License; | ||
// you may not use this file except in compliance with the Elastic License. | ||
|
||
package bbolt | ||
|
||
import ( | ||
"path/filepath" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNew(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
testCase func() *Bbolt | ||
expected *Bbolt | ||
}{ | ||
{ | ||
name: "With no options", | ||
testCase: func() *Bbolt { | ||
return New() | ||
}, | ||
expected: &Bbolt{ | ||
dbPath: defaultDbPath, | ||
dbFileMode: defaultDbFileMode, | ||
bucketName: defaultBucketName, | ||
db: nil, | ||
}, | ||
}, | ||
{ | ||
name: "With options", | ||
testCase: func() *Bbolt { | ||
return New( | ||
WithDbPath("test/path"), | ||
WithBucketName("test_bucket"), | ||
WithDbFileMode(0777), | ||
) | ||
}, | ||
expected: &Bbolt{ | ||
dbPath: "test/path", | ||
dbFileMode: 0777, | ||
bucketName: "test_bucket", | ||
db: nil, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
boltCache := tt.testCase() | ||
assert.Equal(t, tt.expected, boltCache) | ||
}) | ||
} | ||
} | ||
|
||
func TestGetSet(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
testCase func(*testing.T, *Bbolt) | ||
}{ | ||
{ | ||
name: "Simple Set and Get", | ||
testCase: func(t *testing.T, bolt *Bbolt) { | ||
err := bolt.Set([]byte("testKey"), []byte("test_value"), 0) | ||
assert.NoError(t, err) | ||
|
||
val, err := bolt.Get([]byte("testKey")) | ||
assert.NoError(t, err) | ||
assert.Equal(t, []byte("test_value"), val) | ||
}, | ||
}, | ||
{ | ||
name: "Set with expiration", | ||
testCase: func(t *testing.T, bolt *Bbolt) { | ||
err := bolt.Set([]byte("testKeyWithExpiration"), []byte("test_value"), 5*time.Second) | ||
assert.NoError(t, err) | ||
|
||
val, err := bolt.Get([]byte("testKeyWithExpiration")) | ||
assert.NoError(t, err) | ||
assert.Equal(t, []byte("test_value"), val) | ||
}, | ||
}, | ||
{ | ||
name: "Get expired key", | ||
testCase: func(t *testing.T, bolt *Bbolt) { | ||
err := bolt.Set([]byte("testKeyWithExpiration2"), []byte("test_value"), time.Nanosecond) | ||
assert.NoError(t, err) | ||
|
||
time.Sleep(time.Nanosecond) // make sure we wait until key in the cache is expired | ||
|
||
val, err := bolt.Get([]byte("testKeyWithExpiration2")) | ||
assert.NoError(t, err) | ||
assert.Nil(t, val) | ||
}, | ||
}, | ||
{ | ||
name: "Get not existent key", | ||
testCase: func(t *testing.T, bolt *Bbolt) { | ||
val, err := bolt.Get([]byte("doesNotExist")) | ||
assert.NoError(t, err) | ||
assert.Nil(t, val) | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
dbPath := filepath.Join(t.TempDir(), "test.db") | ||
|
||
bolt := &Bbolt{ | ||
dbPath: dbPath, | ||
dbFileMode: 0o644, | ||
bucketName: "test_bucket", | ||
} | ||
|
||
err := bolt.Open() | ||
require.NoError(t, err) | ||
defer bolt.Close() | ||
|
||
tt.testCase(t, bolt) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
// or more contributor license agreements. Licensed under the Elastic License; | ||
// you may not use this file except in compliance with the Elastic License. | ||
|
||
package kv | ||
|
||
import "time" | ||
|
||
type KV interface { | ||
Open() error | ||
Get([]byte) ([]byte, error) | ||
Set([]byte, []byte, time.Duration) error | ||
Close() error | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't ever remove entries that stop being accessed, potentially leading to memory or storage leaks. This can happen in the Cloudfoundry use case when applications are removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would you suggest to fix this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what would be the best option here. A naive approach can be to have a goroutine that periodically looks for expired entries, but not sure about the performance hit it could cause in cases with many entries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah
I've had the same idea but fetching and checking all the keys for expiration might be problematic in case of many keys being stored in store
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another idea would be to have a "system" key which would track all the keys and their expiration time which would eliminate the need to iterate over all keys in the store (but it would complicate the code meaning making it more prone to errors/bugs and in case there are many keys to delete it would still potentially have a hit on the performance).
And another variation of the first idea - store everything in one keys (to outside users it will look the same but under the hood it would be a single bolt key) but again, this way any operation would be an operation on a single bolt key but again - complication and i don't know if bolt has a limit on key (key's value) size
to be honest, I don't really like any of the ideas I've listed, just brainstorming