Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cache: feat cache contract and inmemory cache provider #677

Open
wants to merge 3 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,34 @@ if you'd like to see some endpoint or endpoint ideas implemented. This system
is completely extensible, so I can introduce them without breaking
backwards compatibility.

## Cache

Cache is a provider for cache that provides a contract to work with external storage.
By default, is in memory, can be implemented redis, mecmached, etc.

```go
// Cache is a provider for cache
//
// All cache providers must implement methods, which work with storage
// to store a cache of user state, inline results
// for a Telegram bot.
type Cache interface {
Get(kind CacheKind, key string) (interface{}, error)
Set(kind CacheKind, key string, value interface{}) error
Clear(kind CacheKind, key string) error

Keys(kind CacheKind) []string
}
```

To store context to cache, could be used cache context middleware.

```go
cache := tele.NewInMemoryCache()

b.Use(middleware.CacheContext(cache))
```

## Context
Context is a special type that wraps a huge update structure and represents
the context of the current event. It provides several helpers, which allow
Expand Down
104 changes: 104 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package telebot

import (
"errors"
"sync"
)

type CacheKind string

const (
CacheUserContext CacheKind = "user_context"
)

// Cache is a provider for cache
//
// All cache providers must implement methods, which work with storage
// to store a cache of user state, inline results
// for a Telegram bot.
type Cache interface {
Get(kind CacheKind, key string) (interface{}, error)
Put(kind CacheKind, key string, value interface{}) error
Clear(kind CacheKind, key string) error

Keys(kind CacheKind) []string
}

// InMemoryCache is a provider of in memory cache.
//
// Would be enabled by default
type inMemoryCache struct {
lock sync.RWMutex
data map[CacheKind]map[string]interface{}

keys map[CacheKind][]string // To maintain order of keys
}

func NewInMemoryCache() Cache {
return &inMemoryCache{}
}

func (m *inMemoryCache) Get(kind CacheKind, key string) (interface{}, error) {
m.lock.RLock()
defer m.lock.RUnlock()

if _, ok := m.data[kind]; !ok {
return nil, errors.New("telebot: cache kind is not found")
}

if _, ok := m.data[kind][key]; !ok {
return nil, errors.New("telebot: cache key is not found")
}

return m.data[kind][key], nil
}

func (m *inMemoryCache) Put(kind CacheKind, key string, value interface{}) error {
m.lock.Lock()
defer m.lock.Unlock()

if _, ok := m.data[kind]; !ok {
m.data[kind] = make(map[string]interface{})
m.keys[kind] = make([]string, 1)
}

m.data[kind][key] = value
m.keys[kind] = append(m.keys[kind], key) // Update the keys slice

return nil
}

func (m *inMemoryCache) Clear(kind CacheKind, key string) error {
m.lock.RLock()
defer m.lock.RUnlock()

if _, ok := m.data[kind]; !ok {
return errors.New("telebot: cache kind is not found")
}

if _, ok := m.data[kind][key]; !ok {
return errors.New("telebot: cache key not found")
}

delete(m.data[kind], key) // delete value
keysRemove(m.keys[kind], key) // delete key

return nil
}

func (m *inMemoryCache) Keys(kind CacheKind) []string {
m.lock.RLock()
defer m.lock.RUnlock()

return m.keys[kind]
}

func keysRemove(keys []string, remove string) []string {
for i, k := range keys {
if k == remove {
return append(keys[:i], keys[i+1:]...)
}
}

return keys
}
31 changes: 31 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package telebot

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestInMemoryCache_Get(t *testing.T) {
mc := NewInMemoryCache()

test, _ := mc.Get(CacheUserContext, "test")
assert.Equal(t, nil, test)
}

func TestInMemoryCache_Set(t *testing.T) {
mc := NewInMemoryCache()

_ = mc.Put(CacheUserContext, "test", "test")
test, _ := mc.Get(CacheUserContext, "test")
assert.Equal(t, "test", test)
}

func TestInMemoryCache_Keys(t *testing.T) {
mc := NewInMemoryCache()

_ = mc.Put(CacheUserContext, "test", "test")

keys := mc.Keys(CacheUserContext)
assert.Equal(t, []string{"test"}, keys)
}
15 changes: 15 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ type Context interface {

// Set saves data in the context.
Set(key string, val interface{})

// Keys retrieves store keys
Keys() []string
}

// nativeContext is a native implementation of the Context interface.
Expand Down Expand Up @@ -503,3 +506,15 @@ func (c *nativeContext) Get(key string) interface{} {
defer c.lock.RUnlock()
return c.store[key]
}

func (c *nativeContext) Keys() []string {
c.lock.RLock()
defer c.lock.RUnlock()

keys := make([]string, 0, len(c.store))
for k := range c.store {
keys = append(keys, k)
}

return keys
}
38 changes: 38 additions & 0 deletions middleware/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package middleware

import (
"log"

tele "gopkg.in/telebot.v3"
)

// CacheContext returns a middleware that store context to cache and retreive data from cache to context
// If no custom cache provided, context store will be rested each iteration.
func CacheContext(cache tele.Cache) tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(ctx tele.Context) error {
for _, key := range cache.Keys(tele.CacheUserContext) {
value, err := cache.Get(tele.CacheUserContext, key)
if err != nil {
log.Printf("err: %s was happened, %s -> %s was not got from cache", err, value, key)

return err
}

ctx.Set(key, value)
}

defer func() {
for _, key := range ctx.Keys() {
value := ctx.Get(key)
err := cache.Put(tele.CacheUserContext, key, value)
if err != nil {
log.Printf("err: %s was happened, %s -> %s was not stored from context", err, value, key)
}
}
}()

return next(ctx)
}
}
}