/*
* Copyright 2019 Dgraph Labs, Inc. and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ristretto
import (
"sync"
"time"
)
// TODO: Do we need this to be a separate struct from Item?
type storeItem struct {
key uint64
conflict uint64
value interface{}
expiration time.Time
}
const numShards uint64 = 256
type updateFn func(prev, cur interface{}) bool
type shardedMap struct {
shards []*lockedMap
expiryMap *expirationMap
shouldUpdate func(prev, cur interface{}) bool
}
// newShardedMap is safe for concurrent usage.
func newShardedMap(fn updateFn) *shardedMap {
sm := &shardedMap{
shards: make([]*lockedMap, int(numShards)),
expiryMap: newExpirationMap(),
}
if fn == nil {
fn = func(prev, cur interface{}) bool {
return true
}
}
for i := range sm.shards {
sm.shards[i] = newLockedMap(fn, sm.expiryMap)
}
return sm
}
func (sm *shardedMap) Get(key, conflict uint64) (interface{}, bool) {
return sm.shards[key%numShards].get(key, conflict)
}
func (sm *shardedMap) Expiration(key uint64) time.Time {
return sm.shards[key%numShards].Expiration(key)
}
func (sm *shardedMap) Set(i *Item) {
if i == nil {
// If item is nil make this Set a no-op.
return
}
sm.shards[i.Key%numShards].Set(i)
}
func (sm *shardedMap) Del(key, conflict uint64) (uint64, interface{}) {
return sm.shards[key%numShards].Del(key, conflict)
}
func (sm *shardedMap) Update(newItem *Item) (interface{}, bool) {
return sm.shards[newItem.Key%numShards].Update(newItem)
}
func (sm *shardedMap) Cleanup(policy *lfuPolicy, onEvict itemCallback) {
sm.expiryMap.cleanup(sm, policy, onEvict)
}
func (sm *shardedMap) Clear(onEvict itemCallback) {
for i := uint64(0); i < numShards; i++ {
sm.shards[i].Clear(onEvict)
}
}
type lockedMap struct {
sync.RWMutex
data map[uint64]storeItem
em *expirationMap
shouldUpdate updateFn
}
func newLockedMap(fn updateFn, em *expirationMap) *lockedMap {
return &lockedMap{
data: make(map[uint64]storeItem),
em: em,
shouldUpdate: fn,
}
}
func (m *lockedMap) get(key, conflict uint64) (interface{}, bool) {
m.RLock()
item, ok := m.data[key]
m.RUnlock()
if !ok {
return nil, false
}
if conflict != 0 && (conflict != item.conflict) {
return nil, false
}
// Handle expired items.
if !item.expiration.IsZero() && time.Now().After(item.expiration) {
return nil, false
}
return item.value, true
}
func (m *lockedMap) Expiration(key uint64) time.Time {
m.RLock()
defer m.RUnlock()
return m.data[key].expiration
}
func (m *lockedMap) Set(i *Item) {
if i == nil {
// If the item is nil make this Set a no-op.
return
}
m.Lock()
defer m.Unlock()
item, ok := m.data[i.Key]
if ok {
// The item existed already. We need to check the conflict key and reject the
// update if they do not match. Only after that the expiration map is updated.
if i.Conflict != 0 && (i.Conflict != item.conflict) {
return
}
if !m.shouldUpdate(item.value, i.Value) {
return
}
m.em.update(i.Key, i.Conflict, item.expiration, i.Expiration)
} else {
// The value is not in the map already. There's no need to return anything.
// Simply add the expiration map.
m.em.add(i.Key, i.Conflict, i.Expiration)
}
m.data[i.Key] = storeItem{
key: i.Key,
conflict: i.Conflict,
value: i.Value,
expiration: i.Expiration,
}
}
func (m *lockedMap) Del(key, conflict uint64) (uint64, interface{}) {
m.Lock()
item, ok := m.data[key]
if !ok {
m.Unlock()
return 0, nil
}
if conflict != 0 && (conflict != item.conflict) {
m.Unlock()
return 0, nil
}
if !item.expiration.IsZero() {
m.em.del(key, item.expiration)
}
delete(m.data, key)
m.Unlock()
return item.conflict, item.value
}
func (m *lockedMap) Update(newItem *Item) (interface{}, bool) {
m.Lock()
defer m.Unlock()
item, ok := m.data[newItem.Key]
if !ok {
return nil, false
}
if newItem.Conflict != 0 && (newItem.Conflict != item.conflict) {
return nil, false
}
if !m.shouldUpdate(item.value, newItem.Value) {
return item.value, false
}
m.em.update(newItem.Key, newItem.Conflict, item.expiration, newItem.Expiration)
m.data[newItem.Key] = storeItem{
key: newItem.Key,
conflict: newItem.Conflict,
value: newItem.Value,
expiration: newItem.Expiration,
}
return item.value, true
}
func (m *lockedMap) Clear(onEvict itemCallback) {
m.Lock()
i := &Item{}
if onEvict != nil {
for _, si := range m.data {
i.Key = si.key
i.Conflict = si.conflict
i.Value = si.value
onEvict(i)
}
}
m.data = make(map[uint64]storeItem)
m.Unlock()
}