2017-11-23 05:37:08 +00:00
package common
import (
"sync"
"sync/atomic"
)
2018-06-17 07:28:18 +00:00
// UserCache is an interface which spits out users from a fast cache rather than the database, whether from memory or from an application like Redis. Users may not be present in the cache but may be in the database
2017-11-23 05:37:08 +00:00
type UserCache interface {
2018-09-19 06:09:03 +00:00
DeallocOverflow ( evictPriority bool ) ( evicted int ) // May cause thread contention, looks for items to evict
2017-11-23 05:37:08 +00:00
Get ( id int ) ( * User , error )
GetUnsafe ( id int ) ( * User , error )
BulkGet ( ids [ ] int ) ( list [ ] * User )
Set ( item * User ) error
Add ( item * User ) error
AddUnsafe ( item * User ) error
Remove ( id int ) error
RemoveUnsafe ( id int ) error
Flush ( )
Length ( ) int
SetCapacity ( capacity int )
GetCapacity ( ) int
}
2018-06-17 07:28:18 +00:00
// MemoryUserCache stores and pulls users out of the current process' memory
2017-11-23 05:37:08 +00:00
type MemoryUserCache struct {
2018-09-13 07:41:01 +00:00
items map [ int ] * User // TODO: Shard this into two?
2017-11-23 05:37:08 +00:00
length int64
capacity int
sync . RWMutex
}
// NewMemoryUserCache gives you a new instance of MemoryUserCache
func NewMemoryUserCache ( capacity int ) * MemoryUserCache {
return & MemoryUserCache {
items : make ( map [ int ] * User ) ,
capacity : capacity ,
}
}
2018-09-13 07:41:01 +00:00
// TODO: Avoid deallocating topic list users
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) DeallocOverflow ( evictPriority bool ) ( evicted int ) {
toEvict := make ( [ ] int , 10 )
evIndex := 0
s . RLock ( )
for _ , user := range s . items {
2018-09-13 07:41:01 +00:00
if /*user.LastActiveAt < lastActiveCutoff && */ user . Score == 0 && ! user . IsMod {
if EnableWebsockets && WsHub . HasUser ( user . ID ) {
continue
}
toEvict [ evIndex ] = user . ID
evIndex ++
if evIndex == 10 {
break
}
}
}
2019-08-31 22:34:43 +00:00
s . RUnlock ( )
2018-09-13 07:41:01 +00:00
2018-09-19 06:09:03 +00:00
// Clear some of the less active users now with a bit more aggressiveness
if evIndex == 0 && evictPriority {
toEvict = make ( [ ] int , 20 )
2019-08-31 22:34:43 +00:00
s . RLock ( )
for _ , user := range s . items {
2018-09-19 06:09:03 +00:00
if user . Score < 100 && ! user . IsMod {
if EnableWebsockets && WsHub . HasUser ( user . ID ) {
continue
}
toEvict [ evIndex ] = user . ID
evIndex ++
if evIndex == 20 {
break
}
}
}
2019-08-31 22:34:43 +00:00
s . RUnlock ( )
2018-09-19 06:09:03 +00:00
}
2018-09-13 07:41:01 +00:00
// Remove zero IDs from the evictable list, so we don't waste precious cycles locked for those
2019-08-31 22:34:43 +00:00
lastZero := - 1
2018-09-13 07:41:01 +00:00
for i , uid := range toEvict {
if uid == 0 {
lastZero = i
}
}
if lastZero != - 1 {
toEvict = toEvict [ : lastZero ]
}
2019-08-31 22:34:43 +00:00
s . BulkRemove ( toEvict )
2018-09-19 06:09:03 +00:00
return len ( toEvict )
2018-09-13 07:41:01 +00:00
}
2018-06-17 07:28:18 +00:00
// Get fetches a user by ID. Returns ErrNoRows if not present.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) Get ( id int ) ( * User , error ) {
s . RLock ( )
item , ok := s . items [ id ]
s . RUnlock ( )
2017-11-23 05:37:08 +00:00
if ok {
return item , nil
}
return item , ErrNoRows
}
2018-06-17 07:28:18 +00:00
// BulkGet fetches multiple users by their IDs. Indices without users will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) BulkGet ( ids [ ] int ) ( list [ ] * User ) {
2017-11-23 05:37:08 +00:00
list = make ( [ ] * User , len ( ids ) )
2019-08-31 22:34:43 +00:00
s . RLock ( )
2017-11-23 05:37:08 +00:00
for i , id := range ids {
2019-08-31 22:34:43 +00:00
list [ i ] = s . items [ id ]
2017-11-23 05:37:08 +00:00
}
2019-08-31 22:34:43 +00:00
s . RUnlock ( )
2017-11-23 05:37:08 +00:00
return list
}
2018-06-17 07:28:18 +00:00
// GetUnsafe fetches a user by ID. Returns ErrNoRows if not present. THIS METHOD IS NOT THREAD-SAFE.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) GetUnsafe ( id int ) ( * User , error ) {
item , ok := s . items [ id ]
2017-11-23 05:37:08 +00:00
if ok {
return item , nil
}
return item , ErrNoRows
}
2018-06-17 07:28:18 +00:00
// Set overwrites the value of a user in the cache, whether it's present or not. May return a capacity overflow error.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) Set ( item * User ) error {
s . Lock ( )
user , ok := s . items [ item . ID ]
2017-11-23 05:37:08 +00:00
if ok {
2019-08-31 22:34:43 +00:00
s . Unlock ( )
2017-11-23 05:37:08 +00:00
* user = * item
2019-08-31 22:34:43 +00:00
} else if int ( s . length ) >= s . capacity {
s . Unlock ( )
2017-11-23 05:37:08 +00:00
return ErrStoreCapacityOverflow
} else {
2019-08-31 22:34:43 +00:00
s . items [ item . ID ] = item
s . Unlock ( )
atomic . AddInt64 ( & s . length , 1 )
2017-11-23 05:37:08 +00:00
}
return nil
}
2018-06-17 07:28:18 +00:00
// Add adds a user to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error.
// ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used?
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) Add ( item * User ) error {
s . Lock ( )
if int ( s . length ) >= s . capacity {
s . Unlock ( )
2017-11-23 05:37:08 +00:00
return ErrStoreCapacityOverflow
}
2019-08-31 22:34:43 +00:00
s . items [ item . ID ] = item
s . length = int64 ( len ( s . items ) )
s . Unlock ( )
2017-11-23 05:37:08 +00:00
return nil
}
2018-06-17 07:28:18 +00:00
// AddUnsafe is the unsafe version of Add. May return a capacity overflow error. THIS METHOD IS NOT THREAD-SAFE.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) AddUnsafe ( item * User ) error {
if int ( s . length ) >= s . capacity {
2017-11-23 05:37:08 +00:00
return ErrStoreCapacityOverflow
}
2019-08-31 22:34:43 +00:00
s . items [ item . ID ] = item
s . length = int64 ( len ( s . items ) )
2017-11-23 05:37:08 +00:00
return nil
}
2018-06-17 07:28:18 +00:00
// Remove removes a user from the cache by ID, if they exist. Returns ErrNoRows if no items exist.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) Remove ( id int ) error {
s . Lock ( )
_ , ok := s . items [ id ]
2017-11-23 05:37:08 +00:00
if ! ok {
2019-08-31 22:34:43 +00:00
s . Unlock ( )
2017-11-23 05:37:08 +00:00
return ErrNoRows
}
2019-08-31 22:34:43 +00:00
delete ( s . items , id )
s . Unlock ( )
atomic . AddInt64 ( & s . length , - 1 )
2017-11-23 05:37:08 +00:00
return nil
}
2018-06-17 07:28:18 +00:00
// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) RemoveUnsafe ( id int ) error {
_ , ok := s . items [ id ]
2017-11-23 05:37:08 +00:00
if ! ok {
return ErrNoRows
}
2019-08-31 22:34:43 +00:00
delete ( s . items , id )
atomic . AddInt64 ( & s . length , - 1 )
2017-11-23 05:37:08 +00:00
return nil
}
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) BulkRemove ( ids [ ] int ) {
2018-09-13 07:41:01 +00:00
var rCount int64
2019-08-31 22:34:43 +00:00
s . Lock ( )
2018-09-13 07:41:01 +00:00
for _ , id := range ids {
2019-08-31 22:34:43 +00:00
_ , ok := s . items [ id ]
2018-09-13 07:41:01 +00:00
if ok {
2019-08-31 22:34:43 +00:00
delete ( s . items , id )
2018-09-13 07:41:01 +00:00
rCount ++
}
}
2019-08-31 22:34:43 +00:00
s . Unlock ( )
atomic . AddInt64 ( & s . length , - rCount )
2018-09-13 07:41:01 +00:00
}
2018-06-17 07:28:18 +00:00
// Flush removes all the users from the cache, useful for tests.
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) Flush ( ) {
s . Lock ( )
s . items = make ( map [ int ] * User )
s . length = 0
s . Unlock ( )
2017-11-23 05:37:08 +00:00
}
// ! Is this concurrent?
// Length returns the number of users in the memory cache
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) Length ( ) int {
return int ( s . length )
2017-11-23 05:37:08 +00:00
}
2018-06-17 07:28:18 +00:00
// SetCapacity sets the maximum number of users which this cache can hold
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) SetCapacity ( capacity int ) {
2018-06-17 07:28:18 +00:00
// Ints are moved in a single instruction, so this should be thread-safe
2019-08-31 22:34:43 +00:00
s . capacity = capacity
2017-11-23 05:37:08 +00:00
}
2018-06-17 07:28:18 +00:00
// GetCapacity returns the maximum number of users this cache can hold
2019-08-31 22:34:43 +00:00
func ( s * MemoryUserCache ) GetCapacity ( ) int {
return s . capacity
2017-11-23 05:37:08 +00:00
}