Compare commits

..

3 Commits

20 changed files with 721 additions and 575 deletions

18
gcache/cache.go Normal file
View File

@ -0,0 +1,18 @@
//
// cache.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import "time"
type ICache interface {
Set(key string, data []byte, expires time.Duration) error
Get(key string) ([]byte, error)
Delete(key string) error
Flush() error
Has(key string) bool
}

51
gcache/cache_test.go Normal file
View File

@ -0,0 +1,51 @@
//
// cache_test.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache_test
import (
"os"
"testing"
"time"
"git.hexq.cn/tiglog/golib/gcache"
"git.hexq.cn/tiglog/golib/gtest"
"github.com/redis/go-redis/v9"
)
var sample_name = "golib gcache"
func TestRedisCache(t *testing.T) {
REDIS_URL := os.Getenv("REDIS_URL")
// fmt.Println(REDIS_URL)
redisOpt, err := redis.ParseURL(REDIS_URL)
gtest.Nil(t, err)
var store gcache.ICache = gcache.NewRedisStore(gcache.RCClientOptions(redisOpt))
var sampleData = []byte(sample_name)
err = store.Set("name", sampleData, time.Minute*1)
gtest.Nil(t, err)
r1, err := store.Get("name")
gtest.Nil(t, err)
gtest.Equal(t, string(r1), sample_name)
}
func TestMemoryCache(t *testing.T) {
store := gcache.NewMemoryStore()
var err error
var sampleData = []byte(sample_name)
err = store.Set("name", sampleData, time.Minute*1)
gtest.Nil(t, err)
r1, err := store.Get("name")
gtest.Nil(t, err)
gtest.Equal(t, string(r1), sample_name)
}

View File

@ -1,21 +0,0 @@
//
// config.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import "time"
type Config struct {
shards int
expiredCallback ExpiredCallback
hash IHash
clearInterval time.Duration
}
func NewConfig() *Config {
return &Config{shards: 1024, hash: newDefaultHash(), clearInterval: 1 * time.Second}
}

214
gcache/fs_adapter.go Normal file
View File

@ -0,0 +1,214 @@
//
// fs_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"bytes"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
)
// Option is an optional type
type FCOption func(fs *FSStore)
func BaseDirectory(base string) FCOption {
return func(fs *FSStore) {
fs.baseDir = base
}
}
func FCSerializer(serializer Serializer) FCOption {
return func(fs *FSStore) {
fs.b = serializer
}
}
func FCCacheKeyGenerator(fn KeyFunc) FCOption {
return func(fs *FSStore) {
fs.keyFn = fn
}
}
const (
defaultFilePerm os.FileMode = 0644
defaultDirectoryFilePerm = 0755
)
func FilePathKeyFunc(s string) string {
hashSum := md5.Sum([]byte(s))
hashSumAsString := hex.EncodeToString(hashSum[:])
return filepath.Join(string(hashSumAsString[0:2]),
string(hashSumAsString[2:4]),
string(hashSumAsString[4:6]), hashSumAsString)
}
func createDirectory(dir string) error {
return os.MkdirAll(dir, defaultDirectoryFilePerm)
}
type FSStore struct {
baseDir string
b Serializer
keyFn KeyFunc
}
func NewFSStore(baseDir string, opts ...FCOption) (*FSStore, error) {
_, err := os.Stat(baseDir)
if err != nil { //Directory does not exist..Let's create it
if err := createDirectory(baseDir); err != nil {
panic(fmt.Errorf("Base directory could not be created : %s", err))
}
}
opts = append(opts, BaseDirectory(baseDir))
store := &FSStore{}
for _, opt := range opts {
opt(store)
}
if store.b == nil {
store.b = NewCacheSerializer()
}
if len(strings.TrimSpace(store.baseDir)) == 0 {
return nil, errors.New("onecache : base directory not provided")
}
if store.keyFn == nil {
store.keyFn = FilePathKeyFunc
}
return store, nil
}
func (fs *FSStore) Set(key string, data []byte, expiresAt time.Duration) error {
path := fs.filePathFor(key)
if err := createDirectory(filepath.Dir(path)); err != nil {
return err
}
i := &Item{ExpiresAt: time.Now().Add(expiresAt), Data: data}
b, err := fs.b.Serialize(i)
if err != nil {
return err
}
return writeFile(path, b)
}
func (fs *FSStore) Get(key string) ([]byte, error) {
var b = new(bytes.Buffer)
f, err := os.OpenFile(fs.filePathFor(key), os.O_RDONLY, 0644)
if err != nil {
pe, ok := err.(*os.PathError)
if !ok {
return nil, err
}
if pe.Err == syscall.ENOENT && pe.Op == "open" {
return nil, ErrCacheMiss
}
return nil, err
}
if _, err := io.Copy(b, f); err != nil {
f.Close()
return nil, err
}
f.Close()
i := new(Item)
if err := fs.b.DeSerialize(b.Bytes(), i); err != nil {
return nil, err
}
if i.IsExpired() {
fs.Delete(key)
return nil, ErrCacheMiss
}
return i.Data, nil
}
func (fs *FSStore) Delete(key string) error {
return os.RemoveAll(fs.filePathFor(key))
}
func (fs *FSStore) Flush() error {
return os.RemoveAll(fs.baseDir)
}
func (fs *FSStore) GC() {
filepath.Walk(
fs.baseDir,
func(path string, finfo os.FileInfo, err error) error {
if err != nil {
return err
}
if finfo.IsDir() {
return nil
}
currentItem := new(Item)
byt, err := os.ReadFile(path)
if err != nil {
return err
}
if err = fs.b.DeSerialize(byt, currentItem); err != nil {
return err
}
if currentItem.IsExpired() {
if err := os.Remove(path); !os.IsExist(err) {
return err
}
}
return nil
})
}
func (fs *FSStore) Has(key string) bool {
_, err := os.Stat(fs.filePathFor(key))
return !os.IsNotExist(err)
}
func (fs *FSStore) filePathFor(key string) string {
return filepath.Join(fs.baseDir, fs.keyFn(key))
}
func writeFile(path string, b []byte) error {
return os.WriteFile(path, b, defaultFilePerm)
}

View File

@ -1,42 +0,0 @@
//
// hash.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// IHash is responsible for generating unsigned, 64-bit hash of provided string. IHash should minimize collisions
// (generating same hash for different strings) and while performance is also important fast functions are preferable (i.e.
// you can use FarmHash family).
type IHash interface {
Sum64(string) uint64
}
// newDefaultHash returns a new 64-bit FNV-1a IHash which makes no memory allocations.
// Its Sum64 method will lay the value out in big-endian byte order.
// See https://en.wikipedia.org/wiki/FowlerNollVo_hash_function
func newDefaultHash() IHash {
return fnv64a{}
}
type fnv64a struct{}
const (
// offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/FowlerNollVo_hash_function#FNV-1a_hash
offset64 = 14695981039346656037
// prime64 FNVa prime value. See https://en.wikipedia.org/wiki/FowlerNollVo_hash_function#FNV-1a_hash
prime64 = 1099511628211
)
// Sum64 gets the string and returns its uint64 hash value.
func (f fnv64a) Sum64(key string) uint64 {
var hash uint64 = offset64
for i := 0; i < len(key); i++ {
hash ^= uint64(key[i])
hash *= prime64
}
return hash
}

View File

@ -1,122 +0,0 @@
//
// icache.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import "time"
// ExpiredCallback Callback the function when the key-value pair expires
// Note that it is executed after expiration
type ExpiredCallback func(k string, v interface{}) error
type ICache interface {
//Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type.
//Any previous time to live associated with the key is discarded on successful SET operation.
//Example:
//c.Set("demo", 1)
//c.Set("demo", 1, WithEx(10*time.Second))
//c.Set("demo", 1, WithEx(10*time.Second), WithNx())
Set(k string, v interface{}, opts ...SetIOption) bool
//Get the value of key.
//If the key does not exist the special value nil,false is returned.
//Example:
//c.Get("demo") //nil, false
//c.Set("demo", "value")
//c.Get("demo") //"value", true
Get(k string) (interface{}, bool)
//GetSet Atomically sets key to value and returns the old value stored at key.
//Returns nil,false when key not exists.
//Example:
//c.GetSet("demo", 1) //nil,false
//c.GetSet("demo", 2) //1,true
GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool)
//GetDel Get the value of key and delete the key.
//This command is similar to GET, except for the fact that it also deletes the key on success.
//Example:
//c.Set("demo", "value")
//c.GetDel("demo") //"value", true
//c.GetDel("demo") //nil, false
GetDel(k string) (interface{}, bool)
//Del Removes the specified keys. A key is ignored if it does not exist.
//Return the number of keys that were removed.
//Example:
//c.Set("demo1", "1")
//c.Set("demo2", "1")
//c.Del("demo1", "demo2", "demo3") //2
Del(keys ...string) int
//DelExpired Only delete when key expires
//Example:
//c.Set("demo1", "1")
//c.Set("demo2", "1", WithEx(1*time.Second))
//time.Sleep(1*time.Second)
//c.DelExpired("demo1", "demo2") //true
DelExpired(k string) bool
//Exists Returns if key exists.
//Return the number of exists keys.
//Example:
//c.Set("demo1", "1")
//c.Set("demo2", "1")
//c.Exists("demo1", "demo2", "demo3") //2
Exists(keys ...string) bool
//Expire Set a timeout on key.
//After the timeout has expired, the key will automatically be deleted.
//Return false if the key not exist.
//Example:
//c.Expire("demo", 1*time.Second) // false
//c.Set("demo", "1")
//c.Expire("demo", 1*time.Second) // true
Expire(k string, d time.Duration) bool
//ExpireAt has the same effect and semantic as Expire, but instead of specifying the number of seconds representing the TTL (time to live),
//it takes an absolute Unix Time (seconds since January 1, 1970). A Time in the past will delete the key immediately.
//Return false if the key not exist.
//Example:
//c.ExpireAt("demo", time.Now().Add(10*time.Second)) // false
//c.Set("demo", "1")
//c.ExpireAt("demo", time.Now().Add(10*time.Second)) // true
ExpireAt(k string, t time.Time) bool
//Persist Remove the existing timeout on key.
//Return false if the key not exist.
//Example:
//c.Persist("demo") // false
//c.Set("demo", "1")
//c.Persist("demo") // true
Persist(k string) bool
//Ttl Returns the remaining time to live of a key that has a timeout.
//Returns 0,false if the key does not exist or if the key exist but has no associated expire.
//Example:
//c.Set("demo", "1")
//c.Ttl("demo") // 0,false
//c.Set("demo", "1", WithEx(10*time.Second))
//c.Ttl("demo") // 10*time.Second,true
Ttl(k string) (time.Duration, bool)
}
type IItem interface {
Expired() bool
CanExpire() bool
SetExpireAt(t time.Time)
}
type Item struct {
v interface{}
expire time.Time
}
func (i *Item) Expired() bool {
if !i.CanExpire() {
return false
}
return time.Now().After(i.expire)
}
func (i *Item) CanExpire() bool {
return !i.expire.IsZero()
}
func (i *Item) SetExpireAt(t time.Time) {
i.expire = t
}

View File

@ -1,255 +0,0 @@
//
// memory.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"runtime"
"sync"
"time"
)
func NewMemCache(opts ...ICacheOption) ICache {
conf := NewConfig()
for _, opt := range opts {
opt(conf)
}
c := &memCache{
shards: make([]*memCacheShard, conf.shards),
closed: make(chan struct{}),
shardMask: uint64(conf.shards - 1),
config: conf,
hash: conf.hash,
}
for i := 0; i < len(c.shards); i++ {
c.shards[i] = newMemCacheShard(conf)
}
if conf.clearInterval > 0 {
go func() {
ticker := time.NewTicker(conf.clearInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
for _, shard := range c.shards {
shard.checkExpire()
}
case <-c.closed:
return
}
}
}()
}
cache := &MemCache{c}
// Associated finalizer function with obj.
// When the obj is unreachable, close the obj.
runtime.SetFinalizer(cache, func(cache *MemCache) { close(cache.closed) })
return cache
}
type MemCache struct {
*memCache
}
type memCache struct {
shards []*memCacheShard
hash IHash
shardMask uint64
config *Config
closed chan struct{}
}
func (c *memCache) Set(k string, v interface{}, opts ...SetIOption) bool {
item := Item{v: v}
for _, opt := range opts {
if pass := opt(c, k, &item); !pass {
return false
}
}
hashedKey := c.hash.Sum64(k)
shard := c.getShard(hashedKey)
shard.set(k, &item)
return true
}
func (c *memCache) Get(k string) (interface{}, bool) {
hashedKey := c.hash.Sum64(k)
shard := c.getShard(hashedKey)
return shard.get(k)
}
func (c *memCache) GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool) {
defer c.Set(k, v, opts...)
return c.Get(k)
}
func (c *memCache) GetDel(k string) (interface{}, bool) {
defer c.Del(k)
return c.Get(k)
}
func (c *memCache) Del(ks ...string) int {
var count int
for _, k := range ks {
hashedKey := c.hash.Sum64(k)
shard := c.getShard(hashedKey)
count += shard.del(k)
}
return count
}
// DelExpired Only delete when key expires
func (c *memCache) DelExpired(k string) bool {
hashedKey := c.hash.Sum64(k)
shard := c.getShard(hashedKey)
return shard.delExpired(k)
}
func (c *memCache) Exists(ks ...string) bool {
for _, k := range ks {
if _, found := c.Get(k); !found {
return false
}
}
return true
}
func (c *memCache) Expire(k string, d time.Duration) bool {
v, found := c.Get(k)
if !found {
return false
}
return c.Set(k, v, WithEx(d))
}
func (c *memCache) ExpireAt(k string, t time.Time) bool {
v, found := c.Get(k)
if !found {
return false
}
return c.Set(k, v, WithExAt(t))
}
func (c *memCache) Persist(k string) bool {
v, found := c.Get(k)
if !found {
return false
}
return c.Set(k, v)
}
func (c *memCache) Ttl(k string) (time.Duration, bool) {
hashedKey := c.hash.Sum64(k)
shard := c.getShard(hashedKey)
return shard.ttl(k)
}
func (c *memCache) getShard(hashedKey uint64) (shard *memCacheShard) {
return c.shards[hashedKey&c.shardMask]
}
//////////////
// shard //
//////////////
type memCacheShard struct {
hashmap map[string]Item
lock sync.RWMutex
expiredCallback ExpiredCallback
}
func newMemCacheShard(conf *Config) *memCacheShard {
return &memCacheShard{expiredCallback: conf.expiredCallback, hashmap: map[string]Item{}}
}
func (c *memCacheShard) set(k string, item *Item) {
c.lock.Lock()
c.hashmap[k] = *item
c.lock.Unlock()
return
}
func (c *memCacheShard) get(k string) (interface{}, bool) {
c.lock.RLock()
item, exist := c.hashmap[k]
c.lock.RUnlock()
if !exist {
return nil, false
}
if !item.Expired() {
return item.v, true
}
if c.delExpired(k) {
return nil, false
}
return c.get(k)
}
func (c *memCacheShard) getSet(k string, item *Item) (interface{}, bool) {
defer c.set(k, item)
return c.get(k)
}
func (c *memCacheShard) getDel(k string) (interface{}, bool) {
defer c.del(k)
return c.get(k)
}
func (c *memCacheShard) del(k string) int {
var count int
c.lock.Lock()
v, found := c.hashmap[k]
if found {
delete(c.hashmap, k)
if !v.Expired() {
count++
}
}
c.lock.Unlock()
return count
}
// delExpired Only delete when key expires
func (c *memCacheShard) delExpired(k string) bool {
c.lock.Lock()
item, found := c.hashmap[k]
if !found || !item.Expired() {
c.lock.Unlock()
return false
}
delete(c.hashmap, k)
c.lock.Unlock()
if c.expiredCallback != nil {
_ = c.expiredCallback(k, item.v)
}
return true
}
func (c *memCacheShard) ttl(k string) (time.Duration, bool) {
c.lock.RLock()
v, found := c.hashmap[k]
c.lock.RUnlock()
if !found || !v.CanExpire() || v.Expired() {
return 0, false
}
return v.expire.Sub(time.Now()), true
}
func (c *memCacheShard) checkExpire() {
var expiredKeys []string
c.lock.RLock()
for k, item := range c.hashmap {
if item.Expired() {
expiredKeys = append(expiredKeys, k)
}
}
c.lock.RUnlock()
for _, k := range expiredKeys {
c.delExpired(k)
}
}

155
gcache/memory_adapter.go Normal file
View File

@ -0,0 +1,155 @@
//
// memory_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"sync"
"time"
)
// Option defines options for creating a memory store
type MCOption func(i *InMemoryStore)
// BufferSize configures the store to allow a maximum of n
func BufferSize(n int) MCOption {
return func(i *InMemoryStore) {
i.bufferSize = n
}
}
// KeyFunc allows for dynamic generation of cache keys
func MCCacheKeyGenerator(fn KeyFunc) MCOption {
return func(i *InMemoryStore) {
i.keyfn = fn
}
}
// New returns a configured in memory store
func NewMemoryStore(opts ...MCOption) *InMemoryStore {
i := &InMemoryStore{}
for _, opt := range opts {
opt(i)
}
if i.keyfn == nil {
i.keyfn = DefaultKeyFunc
}
if i.data == nil {
if i.bufferSize == 0 {
i.bufferSize = 100
}
i.data = make(map[string]*Item, i.bufferSize)
}
return i
}
// Represents an in-memory store
type InMemoryStore struct {
lock sync.RWMutex
data map[string]*Item
bufferSize int
keyfn KeyFunc
}
func (i *InMemoryStore) Set(key string, data []byte, expires time.Duration) error {
i.lock.Lock()
i.data[i.keyfn(key)] = &Item{
ExpiresAt: time.Now().Add(expires),
Data: copyData(data),
}
i.lock.Unlock()
return nil
}
func (i *InMemoryStore) Get(key string) ([]byte, error) {
i.lock.RLock()
item := i.data[i.keyfn(key)]
if item == nil {
i.lock.RUnlock()
return nil, ErrCacheMiss
}
if item.IsExpired() {
i.lock.RUnlock()
i.Delete(key)
return nil, ErrCacheMiss
}
i.lock.RUnlock()
return copyData(item.Data), nil
}
func (i *InMemoryStore) Delete(key string) error {
i.lock.RLock()
_, ok := i.data[i.keyfn(key)]
if !ok {
i.lock.RUnlock()
return ErrCacheMiss
}
i.lock.RUnlock()
i.lock.Lock()
delete(i.data, i.keyfn(key))
i.lock.Unlock()
return nil
}
func (i *InMemoryStore) Flush() error {
i.lock.Lock()
i.data = make(map[string]*Item, i.bufferSize)
i.lock.Unlock()
return nil
}
func (i *InMemoryStore) Has(key string) bool {
i.lock.RLock()
_, ok := i.data[i.keyfn(key)]
i.lock.RUnlock()
return ok
}
func (i *InMemoryStore) GC() {
i.lock.Lock()
for k, item := range i.data {
if item.IsExpired() {
//No need to spawn a new goroutine since we
//still have the lock here
delete(i.data, k)
}
}
i.lock.Unlock()
}
func (i *InMemoryStore) count() int {
i.lock.Lock()
n := len(i.data)
i.lock.Unlock()
return n
}
func copyData(data []byte) []byte {
result := make([]byte, len(data))
copy(result, data)
return result
}

View File

@ -1,31 +0,0 @@
//
// memory_test.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache_test
import (
"testing"
"time"
"git.hexq.cn/tiglog/golib/gcache"
"git.hexq.cn/tiglog/golib/gtest"
)
func TestUsage(t *testing.T) {
c := gcache.NewMemCache()
c.Set("a", 1)
c.Set("b", 1, gcache.WithEx(1*time.Second))
time.Sleep(1 * time.Second)
r1, ok := c.Get("a") // 1, true
gtest.True(t, ok)
gtest.Equal(t, 1, r1)
r2, ok := c.Get("b")
gtest.False(t, ok)
gtest.Nil(t, r2)
}

View File

@ -1,68 +0,0 @@
//
// option.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import "time"
// SetIOption The option used to cache set
type SetIOption func(ICache, string, IItem) bool
// WithEx Set the specified expire time, in time.Duration.
func WithEx(d time.Duration) SetIOption {
return func(c ICache, k string, v IItem) bool {
v.SetExpireAt(time.Now().Add(d))
return true
}
}
// WithExAt Set the specified expire deadline, in time.Time.
func WithExAt(t time.Time) SetIOption {
return func(c ICache, k string, v IItem) bool {
v.SetExpireAt(t)
return true
}
}
// ICacheOption The option used to create the cache object
type ICacheOption func(conf *Config)
// WithShards set custom size of sharding. Default is 1024
// The larger the size, the smaller the lock force, the higher the concurrency performance,
// and the higher the memory footprint, so try to choose a size that fits your business scenario
func WithShards(shards int) ICacheOption {
if shards <= 0 {
panic("Invalid shards")
}
return func(conf *Config) {
conf.shards = shards
}
}
// WithExpiredCallback set custom expired callback function
// This callback function is called when the key-value pair expires
func WithExpiredCallback(ec ExpiredCallback) ICacheOption {
return func(conf *Config) {
conf.expiredCallback = ec
}
}
// WithHash set custom hash key function
func WithHash(hash IHash) ICacheOption {
return func(conf *Config) {
conf.hash = hash
}
}
// WithClearInterval set custom clear interval.
// Interval for clearing expired key-value pairs. The default value is 1 second
// If the d is 0, the periodic clearing function is disabled
func WithClearInterval(d time.Duration) ICacheOption {
return func(conf *Config) {
conf.clearInterval = d
}
}

View File

@ -1,10 +0,0 @@
//
// redis.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// TODO 暂时不需要,后面再实现

87
gcache/redis_adapter.go Normal file
View File

@ -0,0 +1,87 @@
//
// redis_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
// Option is a redis option type
type RCOption func(r *RedisStore)
// ClientOptions is an Option type that allows configuring a redis client
func RCClientOptions(opts *redis.Options) RCOption {
return func(r *RedisStore) {
r.client = redis.NewClient(opts)
}
}
// CacheKeyGenerator allows configuring the cache key generation process
func RCCacheKeyGenerator(fn KeyFunc) RCOption {
return func(r *RedisStore) {
r.keyFn = fn
}
}
type RedisStore struct {
client *redis.Client
keyFn KeyFunc
}
// New returns a new RedisStore by applying all options passed into it
// It also sets sensible defaults too
func NewRedisStore(opts ...RCOption) *RedisStore {
r := &RedisStore{}
for _, opt := range opts {
opt(r)
}
if r.client == nil {
panic("需要指定 redis 的连接参数")
}
if r.keyFn == nil {
r.keyFn = DefaultKeyFunc
}
return r
}
func (r *RedisStore) Set(k string, data []byte, expires time.Duration) error {
cmd := r.client.Set(context.Background(), r.key(k), data, expires)
return cmd.Err()
}
func (r *RedisStore) Get(key string) ([]byte, error) {
return r.client.Get(context.Background(), r.key(key)).Bytes()
}
func (r *RedisStore) Delete(key string) error {
return r.client.Del(context.Background(), r.key(key)).Err()
}
func (r *RedisStore) Flush() error {
return r.client.FlushDB(context.Background()).Err()
}
func (r *RedisStore) Has(key string) bool {
if _, err := r.Get(key); err != nil {
return false
}
return true
}
func (r *RedisStore) key(k string) string {
return r.keyFn(k)
}

59
gcache/types.go Normal file
View File

@ -0,0 +1,59 @@
//
// tyeps.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"errors"
"time"
)
const (
EXPIRES_DEFAULT = time.Duration(0)
EXPIRES_FOREVER = time.Duration(-1)
)
var (
ErrCacheMiss = errors.New("Key not found")
ErrCacheNotStored = errors.New("Data not stored")
ErrCacheNotSupported = errors.New("Operation not supported")
ErrCacheDataCannotBeIncreasedOrDecreased = errors.New(`
Data isn't an integer/string type. Hence, it cannot be increased or decreased`)
)
// DefaultKeyFunc is the default implementation of cache keys
// All it does is to preprend "onecache:" to the key sent in by client code
func DefaultKeyFunc(s string) string {
return "gcache:" + s
}
// Item identifes a cached piece of data
type Item struct {
ExpiresAt time.Time
Data []byte
}
// Interface for all onecache store implementations
type Store interface {
Set(key string, data []byte, expires time.Duration) error
Get(key string) ([]byte, error)
Delete(key string) error
Flush() error
Has(key string) bool
}
// Some stores like redis and memcache automatically clear out the cache
// But for the filesystem and in memory, this cannot be said.
// Stores that have to manually clear out the cached data should implement this method.
// It's implementation should re run this function everytime the interval is reached
// Say every 5 minutes.
type GarbageCollector interface {
GC()
}
// KeyFunc defines a transformer for cache keys
type KeyFunc func(s string) string

120
gcache/utils.go Normal file
View File

@ -0,0 +1,120 @@
//
// utils.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"bytes"
"encoding/gob"
"strconv"
"time"
)
// Helper method to check if an item is expired.
// Current usecase for this is for garbage collection
func (i *Item) IsExpired() bool {
return time.Now().After(i.ExpiresAt)
}
type Serializer interface {
Serialize(i interface{}) ([]byte, error)
DeSerialize(data []byte, i interface{}) error
}
func NewCacheSerializer() *CacheSerializer {
return &CacheSerializer{}
}
// Helper to serialize and deserialize types
type CacheSerializer struct {
}
// Convert a given type into a byte array
// Caveat -> Types you create might have to be registered with the encoding/gob package
func (b *CacheSerializer) Serialize(i interface{}) ([]byte, error) {
if b, ok := i.([]byte); ok {
return b, nil
}
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(i); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Writes a byte array into a type.
func (b *CacheSerializer) DeSerialize(data []byte, i interface{}) error {
if b, ok := i.(*[]byte); ok {
*b = data
return nil
}
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(i)
}
// Increment increases the value of an item by the specified number of steps
func Increment(val interface{}, steps int) (interface{}, error) {
var ret interface{}
switch val.(type) {
case int:
ret = val.(int) + steps
case int32:
ret = val.(int32) + int32(steps)
case int64:
ret = val.(int64) + int64(steps)
case uint:
ret = val.(uint) + uint(steps)
case uint8:
ret = val.(uint8) + uint8(steps)
case uint16:
ret = val.(uint16) + uint16(steps)
case uint32:
ret = val.(uint32) + uint32(steps)
case uint64:
ret = val.(uint64) + uint64(steps)
case string:
num, err := strconv.Atoi(val.(string))
if err != nil {
return -0, err
}
num += steps
ret = strconv.Itoa(num)
default:
return -0, ErrCacheDataCannotBeIncreasedOrDecreased
}
return ret, nil
}
// Decrement decreases the value of an item by the specified number of steps
func Decrement(val interface{}, steps int) (interface{}, error) {
return Increment(val, steps*-1)
}

View File

@ -16,7 +16,7 @@ import (
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"github.com/casbin/casbin/v2/util"
"github.com/go-redis/redis/v8"
"github.com/redis/go-redis/v9"
)
const (

View File

@ -12,10 +12,10 @@ import (
"os"
"testing"
"github.com/casbin/casbin/v2"
"github.com/go-redis/redis/v8"
"git.hexq.cn/tiglog/golib/gcasbin"
"git.hexq.cn/tiglog/golib/gtest"
"github.com/casbin/casbin/v2"
"github.com/redis/go-redis/v9"
)
func getRedis() *redis.Client {

View File

@ -9,7 +9,7 @@ package gconfig
import (
"bytes"
"io/ioutil"
"os"
"text/template"
"git.hexq.cn/tiglog/golib/gfile"
@ -50,7 +50,7 @@ func (c *BaseConfig) GetData(fname string, must bool) *bytes.Buffer {
return nil
}
}
dat, err := ioutil.ReadFile(fp)
dat, err := os.ReadFile(fp)
helper.CheckErr(err)
tpl, err := template.New("config").Parse(string(dat))
helper.CheckErr(err)

8
go.mod
View File

@ -3,18 +3,14 @@ module git.hexq.cn/tiglog/golib
go 1.20
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/casbin/casbin/v2 v2.70.0
github.com/gin-gonic/gin v1.9.1
github.com/go-redis/redis v6.15.9+incompatible
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.7.1
github.com/hibiken/asynq v0.24.1
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/lib/pq v1.10.9
github.com/mattn/go-runewidth v0.0.14
github.com/mattn/go-sqlite3 v1.14.6
github.com/pkg/errors v0.9.1
github.com/poy/onpar v0.3.2
github.com/rs/xid v1.5.0
github.com/stretchr/testify v1.8.3
go.mongodb.org/mongo-driver v1.11.7
@ -53,7 +49,7 @@ require (
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/redis/go-redis/v9 v9.0.3 // indirect
github.com/redis/go-redis/v9 v9.2.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spf13/cast v1.3.1 // indirect

17
go.sum
View File

@ -1,13 +1,12 @@
git.sr.ht/~nelsam/hel v0.4.3 h1:9W0zz8zv8CZhFsp8r9Wq6c8gFemBdtMurjZU/JKfvfM=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@ -37,10 +36,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
@ -81,14 +80,10 @@ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkL
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -105,10 +100,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v0.3.2 h1:yo8ZRqU3C4RlvkXPWUWfonQiTodAgpKQZ1g8VTNU9xU=
github.com/poy/onpar v0.3.2/go.mod h1:6XDWG8DJ1HsFX6/Btn0pHl3Jz5d1SEEGNZ5N1gtYo+I=
github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=

View File

@ -17,16 +17,16 @@ import (
func TestClient(t *testing.T) {
gqueue.Init(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_USERNAME"), os.Getenv("REDIS_PASSWORD"), 0)
c1 := gqueue.Client()
c2 := gqueue.Client()
c1 := gqueue.GetClient()
c2 := gqueue.GetClient()
gtest.NotNil(t, c1)
gtest.Equal(t, c1, c2)
}
func TestServer(t *testing.T) {
gqueue.Init(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_USERNAME"), os.Getenv("REDIS_PASSWORD"), 0)
s1 := gqueue.Server()
s2 := gqueue.Server()
s1 := gqueue.GetServer()
s2 := gqueue.GetServer()
gtest.NotNil(t, s1)
gtest.Equal(t, s1, s2)
}