Skip to content

Commit

Permalink
Bcache now handles key expiration. (#24)
Browse files Browse the repository at this point in the history
- expiration will be handled by bcache, previous version which basically do nothing with expiration

- passive key deletion, data deleted when Get found that the key is expired. No GC-like mechanism at this phase, could be added later if needed

- deletion delay: add delay before actually delete the key, it is used to handle temporary network connection issue, which prevent data syncing between nodes

- specify ttl instead of expirationTimestamp
  • Loading branch information
iwanbk authored Apr 1, 2019
1 parent 7eebcef commit 2a4e113
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 131 deletions.
34 changes: 7 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,11 @@ A Go Library to create distributed in-memory cache inside your app.

Only need to specify one or few nodes as bootstrap nodes, and all nodes will find each other using gossip protocol

2. When there is cache `set`, the event will be propagated to all of the nodes.
2. When there is cache `Set` and `Delete`, the event will be propagated to all of the nodes.

So, all of the nodes will have synced data.
So, all of the nodes will eventually have synced data.


## Expiration

Although this library doesn't invalidate the keys when it reachs the expiration time,
the expiration timestamp will be used in these ways:

(1) On `Set`:
- as a way to decide which value is the newer when doing data synchronization among nodes
- set timestamp expiration

(2) On `Get`:
- the expiration timestamp could be used to check whether the key has been expired

(3) On `Delete`:
- to decide which operation is the lastes when doing syncronization, for example:
- `Delete` with timestamp 3 and `Set` with timestamp 4 -> `Set` is the latest, so the `Delete` is ignored

So, it is **mandatory** to set the expiration time and the delta from current time must be the same
between `Set` and `Delete`.
We can also use [UnixNano](https://golang.org/pkg/time/#Time.UnixNano) for better precission than `Unix`.


## Cache filling

Expand Down Expand Up @@ -79,7 +59,7 @@ bc, err := New(Config{
if err != nil {
log.Fatalf("failed to create cache: %v", err)
}
bc.Set("my_key", "my_val",12345)
bc.Set("my_key", "my_val",86400)
```

In server 2
Expand All @@ -94,7 +74,7 @@ bc, err := New(Config{
if err != nil {
log.Fatalf("failed to create cache: %v", err)
}
bc.Set("my_key2", "my_val2", 12345)
bc.Set("my_key2", "my_val2", 86400)
```

In server 3
Expand All @@ -109,7 +89,7 @@ bc, err := New(Config{
if err != nil {
log.Fatalf("failed to create cache: %v", err)
}
val, exp, exists := bc.Get("my_key2")
val, exists := bc.Get("my_key2")
```

### GetWithFiller example
Expand All @@ -124,12 +104,12 @@ c, err := New(Config{
if err != nil {
log.Fatalf("failed to create cache: %v", err)
}
val, exp,err := bc.GetWithFiller("my_key2",func(key string) (string, int64, error) {
val, exp,err := bc.GetWithFiller("my_key2",func(key string) (string, error) {
// get value from database
.....
//
return value, 0, nil
})
}, 86400)
```

## Credits
Expand Down
70 changes: 38 additions & 32 deletions bcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"net"
"strconv"
"time"

"github.com/weaveworks/mesh"
"golang.org/x/sync/singleflight"
Expand All @@ -22,10 +23,11 @@ var (

// Bcache represents bcache struct
type Bcache struct {
peer *peer
router *mesh.Router
logger Logger
flight singleflight.Group
peer *peer
router *mesh.Router
logger Logger
flight singleflight.Group
deletionDelay time.Duration
}

// New creates new bcache from the given config
Expand Down Expand Up @@ -89,40 +91,45 @@ func New(cfg Config) (*Bcache, error) {
router.ConnectionMaker.InitiateConnections(cfg.Peers, true)

return &Bcache{
peer: peer,
router: router,
logger: logger,
peer: peer,
router: router,
logger: logger,
deletionDelay: time.Duration(cfg.DeletionDelay) * time.Second,
}, nil
}

// Set sets value for the given key.
//
// expiredTimestamp could be used in these way:
//
// - unix timestamp when this key will be expired
// - as a way to decide which value is the newer when doing data synchronization among nodes
func (b *Bcache) Set(key, val string, expiredTimestamp int64) {
b.peer.Set(key, val, expiredTimestamp)
// Set sets value for the given key with the given ttl in second.
// if ttl <= 0, the key will expired instantly
func (b *Bcache) Set(key, val string, ttl int) {
if ttl <= 0 {
b.Delete(key)
return
}
b.set(key, val, ttl)
}

func (b *Bcache) set(key, val string, ttl int) int64 {
expired := time.Now().Add(time.Duration(ttl) * time.Second).UnixNano()
b.peer.Set(key, val, expired)
return expired
}

// Get gets value for the given key.
//
// It returns the value, expiration timestamp, and true if the key exists
func (b *Bcache) Get(key string) (string, int64, bool) {
// It returns the value and true if the key exists
func (b *Bcache) Get(key string) (string, bool) {
return b.peer.Get(key)
}

// Delete the given key.
//
// The given timestamp is used to decide which operation is the lastes when doing syncronization.
//
// For example: `Delete` with timestamp 3 and `Set` with timestamp 4 -> `Set` is the latest, so the `Delete` is ignored
func (b *Bcache) Delete(key string, expiredTimestamp int64) {
b.peer.Delete(key, expiredTimestamp)
func (b *Bcache) Delete(key string) {
deleteTs := time.Now().Add(b.deletionDelay).UnixNano()
b.peer.Delete(key, deleteTs)
}

// Filler defines func to be called when the given key is not exists
type Filler func(key string) (val string, expired int64, err error)
type Filler func(key string) (val string, err error)

// GetWithFiller gets value for the given key and fill the cache
// if the given key is not exists.
Expand All @@ -134,27 +141,26 @@ type Filler func(key string) (val string, expired int64, err error)
//
//
// It useful to avoid cache stampede to the underlying database
func (b *Bcache) GetWithFiller(key string, filler Filler) (string, int64, error) {
func (b *Bcache) GetWithFiller(key string, filler Filler, ttl int) (string, error) {
if filler == nil {
return "", 0, ErrNilFiller
return "", ErrNilFiller
}

// get value from cache
val, exp, ok := b.Get(key)
val, ok := b.Get(key)
if ok {
return val, exp, nil
return val, nil
}

// construct singleflight filler
flightFn := func() (interface{}, error) {
val, expired, err := filler(key)
val, err := filler(key)
if err != nil {
b.logger.Errorf("filler failed: %v", err)
return nil, err
}

// set the key if filler OK
b.peer.Set(key, val, expired)
expired := b.set(key, val, ttl)

return value{
value: val,
Expand All @@ -167,12 +173,12 @@ func (b *Bcache) GetWithFiller(key string, filler Filler) (string, int64, error)
return flightFn()
})
if err != nil {
return "", 0, err
return "", err
}

// return the value
value := valueIf.(value)
return value.value, value.expired, nil
return value.value, nil
}

// Close closes the cache, free all the resource
Expand Down
Loading

0 comments on commit 2a4e113

Please sign in to comment.