-
Notifications
You must be signed in to change notification settings - Fork 0
m1.2 : Added TTL support #8
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
Changes from all commits
061bda8
b69328d
9c112c8
4078432
8248c22
27875c1
775b111
c1cbe9c
85957d3
b815ca3
3f65e4b
3e8a649
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| module com.github.SantanuKar43/simple-kv | ||
|
|
||
| go 1.25.6 | ||
|
|
||
| require github.com/hashicorp/go-immutable-radix/v2 v2.1.0 | ||
|
|
||
| require github.com/hashicorp/golang-lru/v2 v2.0.0 // indirect |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= | ||
| github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= | ||
| github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | ||
| github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||
| github.com/hashicorp/golang-lru/v2 v2.0.0 h1:Lf+9eD8m5pncvHAOCQj49GSN6aQI8XGfI5OpXNkoWaA= | ||
| github.com/hashicorp/golang-lru/v2 v2.0.0/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= | ||
| golang.org/x/exp v0.0.0-20221215174704-0915cd710c24 h1:6w3iSY8IIkp5OQtbYj8NeuKG1jS9d+kYaubXqsoOiQ8= | ||
| golang.org/x/exp v0.0.0-20221215174704-0915cd710c24/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,36 +2,182 @@ package store | |
|
|
||
| import ( | ||
| "sync" | ||
| "time" | ||
| "context" | ||
| "github.com/hashicorp/go-immutable-radix/v2" | ||
| ) | ||
|
|
||
| const sampleSize = 20; | ||
|
|
||
| type entry struct { | ||
| value string | ||
| expireAt int64 | ||
| } | ||
|
|
||
| type Store struct{ | ||
| mu sync.RWMutex | ||
| data map[string]string | ||
| mu sync.RWMutex | ||
| tree *iradix.Tree[entry] | ||
| lastExpiryKey []byte | ||
| } | ||
|
|
||
| func NewStore() *Store { | ||
| return &Store{ | ||
| mu: sync.RWMutex{}, | ||
| data: make(map[string]string), | ||
| tree: iradix.New[entry](), | ||
| } | ||
| } | ||
|
|
||
| func (s *Store) Get(key string) (string, bool) { | ||
| s.mu.RLock() | ||
| defer s.mu.RUnlock() | ||
| val, ok := s.data[key] | ||
| return val, ok | ||
| snapshot := s.tree | ||
| s.mu.RUnlock() | ||
|
|
||
| value, ok := snapshot.Get([]byte(key)) | ||
| if !ok { | ||
| return "", false | ||
| } | ||
| if value.expireAt > 0 && value.expireAt < time.Now().Unix() { | ||
| return "", false | ||
| } | ||
| return value.value, ok | ||
| } | ||
|
|
||
| func (s *Store) Set(key string, val string) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| s.data[key] = val | ||
| txn := s.tree.Txn() | ||
| txn.Insert([]byte(key), entry{value: val, expireAt: 0}) | ||
| s.tree = txn.Commit() | ||
| } | ||
|
|
||
|
|
||
| func (s *Store) Delete(key string) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| delete(s.data, key) | ||
| } | ||
| txn := s.tree.Txn() | ||
| txn.Delete([]byte(key)) | ||
| s.tree = txn.Commit() | ||
|
|
||
| } | ||
|
|
||
| func (s *Store) SetWithTTL(key string, val string, ttl time.Duration) { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| txn := s.tree.Txn() | ||
| txn.Insert([]byte(key), entry{value: val, expireAt: time.Now().Add(ttl).Unix()}) | ||
| s.tree = txn.Commit() | ||
| } | ||
|
|
||
| func (s *Store) Expire(key string, ttl time.Duration) bool { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| txn := s.tree.Txn() | ||
| value, ok := txn.Get([]byte(key)) | ||
| if !ok { | ||
| return false | ||
| } | ||
| value.expireAt = time.Now().Add(ttl).Unix() | ||
| txn.Insert([]byte(key), value) | ||
| s.tree = txn.Commit() | ||
| return true | ||
| } | ||
|
|
||
| func (s *Store) TTL(key string) int64 { | ||
| s.mu.RLock() | ||
| snapshot := s.tree | ||
| s.mu.RUnlock() | ||
| value, ok := snapshot.Get([]byte(key)) | ||
| if !ok { | ||
| return -2 // key does not exist | ||
| } | ||
| if value.expireAt == 0 { | ||
| return -1 // key exists, no TTL set | ||
| } | ||
| remaining := value.expireAt - time.Now().Unix() | ||
| if remaining < 0 { | ||
| return -2 // expired but not yet cleaned up | ||
| } | ||
| return remaining | ||
| } | ||
|
|
||
| func (s *Store) activeExpire() { | ||
| for { | ||
| s.mu.RLock() | ||
| snapshot := s.tree | ||
| lastKey := s.lastExpiryKey | ||
| s.mu.RUnlock() | ||
|
Comment on lines
+103
to
+106
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The iradix returns a reference to the current tree version. The RLock ensures no concurrent writes modify s.tree while we're reading the reference. Once we have the reference, we can safely release the lock because the tree is immutable. |
||
|
|
||
|
|
||
| var expiredKeys []string | ||
| sampled := 0 | ||
| expiredCount := 0 | ||
| var newLastkey []byte | ||
| started := (lastKey == nil) | ||
|
|
||
| snapshot.Root().Walk(func(key []byte, value entry) bool { | ||
| // skip till lastkey | ||
| if !started { | ||
| if string(key) == string(lastKey) { | ||
| started = true | ||
| return false | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // past lastkey start sampling | ||
| if value.expireAt > 0 && value.expireAt < time.Now().Unix() { | ||
| expiredKeys = append(expiredKeys, string(key)) | ||
| expiredCount++ | ||
| } | ||
| sampled++ | ||
| newLastkey = key | ||
| if sampled >= sampleSize { | ||
| return true | ||
| } | ||
| return false | ||
| }) | ||
|
Comment on lines
+115
to
+136
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we stop walking when runs >= sampleSize, will we be able to reach expired nodes always? Shouldn't we check expiredCount here instead?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct point, changed the approach to a last pointer tracking which helps us to make sure we cover the entire tree |
||
|
|
||
| // Update position for next scan | ||
| s.mu.Lock() | ||
| if sampled < sampleSize { | ||
| // Didn't sample enough keys, wrapped around to end | ||
| s.lastExpiryKey = nil | ||
| } else { | ||
| // Save position to resume from next time | ||
| s.lastExpiryKey = newLastkey | ||
| } | ||
| s.mu.Unlock() | ||
|
|
||
| // Delete expired keys | ||
| if len(expiredKeys) > 0 { | ||
| s.mu.Lock() | ||
| txn := s.tree.Txn() | ||
| for _, key := range expiredKeys { | ||
| val, ok := txn.Get([]byte(key)) | ||
| if ok && val.expireAt > 0 && val.expireAt < time.Now().Unix() { | ||
| txn.Delete([]byte(key)) | ||
| } | ||
| } | ||
| s.tree = txn.Commit() | ||
| s.mu.Unlock() | ||
| } | ||
|
|
||
| // If < 25% expired, stop. Otherwise loop again (adaptive) | ||
| if float64(expiredCount) <= float64(sampleSize)*0.25 { | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func (s *Store) StartExpire(ctx context.Context) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where is this method called?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry missed this added in main.go |
||
| ticker := time.NewTicker(1 * time.Second) | ||
| defer ticker.Stop() | ||
|
|
||
| for { | ||
| select { | ||
| case <-ticker.C: | ||
| s.activeExpire() | ||
| case <-ctx.Done(): | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
|
||
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.
need to handle writing to wal for expire and setex commands. Also replay for these commands needs to take care of the ttl.
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.
done
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.
how is wal replay handled?
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.
WAL entry: "SETEX mykey 60 myvalue" // 60 seconds TTL
Written at: 10:00:00 AM
Server crashes at: 10:00:30 AM (key should expire at 10:01:00 AM)
Server restarts at: 11:00:00 AM (1 hour later)
Problem: We only have "60 seconds" in WAL, not the absolute time "10:01:00 AM"
Iss replay ko kaiser handle karre?? TS bei daale kya??
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.
Instead of writing the ttl, we can write the absolute time when it is supposed to expire. During replay just compare with current time and process accordingly.
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.
Done