-
Notifications
You must be signed in to change notification settings - Fork 189
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor timecache implementations (#523)
* reimplement timecache for sane and performant behaviour * remove seenMessagesMx, take advantage of new tc api * fix timecache tests * fix typo * store expiry, don't make life difficult * refactor common background sweep procedure for both impls * add godocs to TimeCache
- Loading branch information
Showing
7 changed files
with
139 additions
and
130 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,72 +1,56 @@ | ||
package timecache | ||
|
||
import ( | ||
"container/list" | ||
"context" | ||
"sync" | ||
"time" | ||
) | ||
|
||
// FirstSeenCache is a thread-safe copy of https://github.com/whyrusleeping/timecache. | ||
// FirstSeenCache is a time cache that only marks the expiry of a message when first added. | ||
type FirstSeenCache struct { | ||
q *list.List | ||
m map[string]time.Time | ||
span time.Duration | ||
guard *sync.RWMutex | ||
} | ||
lk sync.RWMutex | ||
m map[string]time.Time | ||
ttl time.Duration | ||
|
||
func newFirstSeenCache(span time.Duration) TimeCache { | ||
return &FirstSeenCache{ | ||
q: list.New(), | ||
m: make(map[string]time.Time), | ||
span: span, | ||
guard: new(sync.RWMutex), | ||
} | ||
done func() | ||
} | ||
|
||
func (tc FirstSeenCache) Add(s string) { | ||
tc.guard.Lock() | ||
defer tc.guard.Unlock() | ||
var _ TimeCache = (*FirstSeenCache)(nil) | ||
|
||
_, ok := tc.m[s] | ||
if ok { | ||
log.Debug("first-seen: got same entry") | ||
return | ||
func newFirstSeenCache(ttl time.Duration) *FirstSeenCache { | ||
tc := &FirstSeenCache{ | ||
m: make(map[string]time.Time), | ||
ttl: ttl, | ||
} | ||
|
||
// TODO(#515): Do GC in the background | ||
tc.sweep() | ||
ctx, done := context.WithCancel(context.Background()) | ||
tc.done = done | ||
go background(ctx, &tc.lk, tc.m) | ||
|
||
tc.m[s] = time.Now() | ||
tc.q.PushFront(s) | ||
return tc | ||
} | ||
|
||
func (tc FirstSeenCache) sweep() { | ||
for { | ||
back := tc.q.Back() | ||
if back == nil { | ||
return | ||
} | ||
func (tc *FirstSeenCache) Done() { | ||
tc.done() | ||
} | ||
|
||
v := back.Value.(string) | ||
t, ok := tc.m[v] | ||
if !ok { | ||
panic("inconsistent cache state") | ||
} | ||
func (tc *FirstSeenCache) Has(s string) bool { | ||
tc.lk.RLock() | ||
defer tc.lk.RUnlock() | ||
|
||
if time.Since(t) > tc.span { | ||
tc.q.Remove(back) | ||
delete(tc.m, v) | ||
} else { | ||
return | ||
} | ||
} | ||
_, ok := tc.m[s] | ||
return ok | ||
} | ||
|
||
func (tc FirstSeenCache) Has(s string) bool { | ||
tc.guard.RLock() | ||
defer tc.guard.RUnlock() | ||
func (tc *FirstSeenCache) Add(s string) bool { | ||
tc.lk.Lock() | ||
defer tc.lk.Unlock() | ||
|
||
_, ok := tc.m[s] | ||
if ok { | ||
return false | ||
} | ||
|
||
ts, ok := tc.m[s] | ||
// Only consider the entry found if it was present in the cache AND hadn't already expired. | ||
return ok && time.Since(ts) <= tc.span | ||
tc.m[s] = time.Now().Add(tc.ttl) | ||
return true | ||
} |
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 |
---|---|---|
@@ -1,84 +1,58 @@ | ||
package timecache | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
"time" | ||
|
||
"github.com/emirpasic/gods/maps/linkedhashmap" | ||
) | ||
|
||
// LastSeenCache is a LRU cache that keeps entries for up to a specified time duration. After this duration has elapsed, | ||
// "old" entries will be purged from the cache. | ||
// | ||
// It's also a "sliding window" cache. Every time an unexpired entry is seen again, its timestamp slides forward. This | ||
// keeps frequently occurring entries cached and prevents them from being propagated, especially because of network | ||
// issues that might increase the number of duplicate messages in the network. | ||
// | ||
// Garbage collection of expired entries is event-driven, i.e. it only happens when there is a new entry added to the | ||
// cache. This should be ok - if existing entries are being looked up then the cache is not growing, and when a new one | ||
// appears that would grow the cache, garbage collection will attempt to reduce the pressure on the cache. | ||
// | ||
// This implementation is heavily inspired by https://github.com/whyrusleeping/timecache. | ||
// LastSeenCache is a time cache that extends the expiry of a seen message when added | ||
// or checked for presence with Has.. | ||
type LastSeenCache struct { | ||
m *linkedhashmap.Map | ||
span time.Duration | ||
guard *sync.Mutex | ||
lk sync.Mutex | ||
m map[string]time.Time | ||
ttl time.Duration | ||
|
||
done func() | ||
} | ||
|
||
func newLastSeenCache(span time.Duration) TimeCache { | ||
return &LastSeenCache{ | ||
m: linkedhashmap.New(), | ||
span: span, | ||
guard: new(sync.Mutex), | ||
var _ TimeCache = (*LastSeenCache)(nil) | ||
|
||
func newLastSeenCache(ttl time.Duration) *LastSeenCache { | ||
tc := &LastSeenCache{ | ||
m: make(map[string]time.Time), | ||
ttl: ttl, | ||
} | ||
} | ||
|
||
func (tc *LastSeenCache) Add(s string) { | ||
tc.guard.Lock() | ||
defer tc.guard.Unlock() | ||
ctx, done := context.WithCancel(context.Background()) | ||
tc.done = done | ||
go background(ctx, &tc.lk, tc.m) | ||
|
||
tc.add(s) | ||
return tc | ||
} | ||
|
||
// Garbage collect expired entries | ||
// TODO(#515): Do GC in the background | ||
tc.gc() | ||
func (tc *LastSeenCache) Done() { | ||
tc.done() | ||
} | ||
|
||
func (tc *LastSeenCache) add(s string) { | ||
// We don't need a lock here because this function is always called with the lock already acquired. | ||
func (tc *LastSeenCache) Add(s string) bool { | ||
tc.lk.Lock() | ||
defer tc.lk.Unlock() | ||
|
||
// If an entry already exists, remove it and add a new one to the back of the list to maintain temporal ordering and | ||
// an accurate sliding window. | ||
tc.m.Remove(s) | ||
now := time.Now() | ||
tc.m.Put(s, &now) | ||
} | ||
_, ok := tc.m[s] | ||
tc.m[s] = time.Now().Add(tc.ttl) | ||
|
||
func (tc *LastSeenCache) gc() { | ||
// We don't need a lock here because this function is always called with the lock already acquired. | ||
iter := tc.m.Iterator() | ||
for iter.Next() { | ||
key := iter.Key() | ||
ts := iter.Value().(*time.Time) | ||
// Exit if we've found an entry with an unexpired timestamp. Since we're iterating in order of insertion, all | ||
// entries hereafter will be unexpired. | ||
if time.Since(*ts) <= tc.span { | ||
return | ||
} | ||
tc.m.Remove(key) | ||
} | ||
return !ok | ||
} | ||
|
||
func (tc *LastSeenCache) Has(s string) bool { | ||
tc.guard.Lock() | ||
defer tc.guard.Unlock() | ||
tc.lk.Lock() | ||
defer tc.lk.Unlock() | ||
|
||
// If the entry exists and has not already expired, slide it forward. | ||
if ts, found := tc.m.Get(s); found { | ||
if t := ts.(*time.Time); time.Since(*t) <= tc.span { | ||
tc.add(s) | ||
return true | ||
} | ||
_, ok := tc.m[s] | ||
if ok { | ||
tc.m[s] = time.Now().Add(tc.ttl) | ||
} | ||
return false | ||
|
||
return ok | ||
} |
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
Oops, something went wrong.