Skip to content

Commit

Permalink
Merge pull request #38 from hyp3rd/development
Browse files Browse the repository at this point in the history
custom backends support
  • Loading branch information
hyp3rd authored Apr 3, 2023
2 parents 0e6bfe1 + b58e60a commit abcb48d
Show file tree
Hide file tree
Showing 41 changed files with 706 additions and 959 deletions.
180 changes: 9 additions & 171 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

## Synopsis

HyperCache is a **thread-safe** **high-performance** cache implementation in `Go` that supports multiple backends with an optional size limit, expiration, and eviction of items supporting custom algorithms alongside the defaults. It can be used as a standalone cache or as a cache middleware for a service. It can implement a [service interface](./service.go) to intercept and decorate the cache methods with middleware (default or custom).
It is optimized for performance and flexibility, allowing to specify the expiration and eviction intervals, and providing and registering new eviction algorithms, stats collectors, and middleware(s).
It ships with a default [historigram stats collector](./stats/statscollector.go) and several eviction algorithms, but you can develop and register your own as long as it implements the [Eviction Algorithm interface](./eviction/eviction.go).:
HyperCache is a **thread-safe** **high-performance** cache implementation in `Go` that supports multiple backends with an optional size limit, expiration, and eviction of items with custom algorithms alongside the defaults. It can be used as a standalone cache, distributed environents, or as a cache middleware for a service. It can implement a [service interface](./service.go) to intercept and decorate the cache methods with middleware (default or custom).
It is optimized for performance and flexibility, allowing to specify of the expiration and eviction intervals and providing and registering new eviction algorithms, stats collectors, and middleware(s).
It ships with a default [historigram stats collector](./stats/stats.go) and several eviction algorithms. However, you can develop and register your own if it implements the [Eviction Algorithm interface](./eviction/eviction.go).:

- [Recently Used (LRU) eviction algorithm](./eviction/lru.go)
- [The Least Frequently Used (LFU) algorithm](./eviction/lfu.go)
Expand All @@ -18,7 +18,7 @@ It ships with a default [historigram stats collector](./stats/statscollector.go)

- Thread-safe
- High-performance
- Supports multiple backends, default backends are:
- Supports multiple, custom backends. Default backends are:
1. [In-memory](./backend/inmemory.go)
2. [Redis](./backend/redis.go)
- Store items in the cache with a key and expiration duration
Expand All @@ -27,8 +27,8 @@ It ships with a default [historigram stats collector](./stats/statscollector.go)
- Clear the cache of all items
- Evitc items in the background based on the cache capacity and items access leveraging several custom eviction algorithms
- Expire items in the background based on their duration
- [Eviction Algorithm interface](./eviction.go) to implement custom eviction algorithms.
- Stats collection with a default [stats collector](./stats/statscollector.go) or a custom one that implements the StatsCollector interface.
- [Eviction Algorithm interface](./eviction/eviction.go) to implement custom eviction algorithms.
- Stats collection with a default [stats collector](./stats/stats.go) or a custom one that implements the StatsCollector interface.
- [Service interface implementation](./service.go) to allow intercepting cache methods and decorate them with custom or default middleware(s).

## Installation
Expand Down Expand Up @@ -87,7 +87,7 @@ if err != nil {
}
```

For a fine-grained control over the cache configuration, use the `New` function, for instance:
For fine-grained control over the cache configuration, use the `New` function, for instance:

```golang
config := hypercache.NewConfig[backend.InMemory]()
Expand All @@ -108,174 +108,12 @@ if err != nil {
}
```

**For the full configuration options, refer to the [config.go](./config.go) file.**

### Set

Set adds an item to the cache with the given key and value.

```golang
err := cache.Set("key", "value", time.Hour)
if err != nil {
// handle error
}
```

The `Set` function takes a key, a value, and a duration as arguments. The key must be a non-empty string, the value can be of any type, and the duration specifies how long the item should stay in the cache before it expires.

### Get

`Get` retrieves the item with the given key from the cache.

```golang
value, ok := cache.Get("key")
if !ok {
// handle item not found
}
```

The `Get` function returns the value associated with the given key or an error if the key is not found or has expired.

### Remove

`Remove` deletes items with the given key from the cache. If an item is not found, it does nothing.

```golang
err := cache.Remove("key", "key2", "key3")
if err != nil {
// handle error
}
```

The `Remove` function takes a variadic number of keys as arguments and returns an error if any keys are not found.

**Refer to the [config.go](./config.go) file for the full configuration options.**
**For a comprehensive API overview, see the [documentation](https://pkg.go.dev/github.com/hyp3rd/hypercache).**

## Service interface for microservices implementation

The `Service` interface allows intercepting cache methods and decorating them with custom or default middleware(s).

```golang
var svc hypercache.Service
hyperCache, err := hypercache.NewInMemoryWithDefaults(10)

if err != nil {
fmt.Println(err)
return
}
// assign statsCollector of the backend to use it in middleware
statsCollector := hyperCache.StatsCollector
svc = hyperCache

if err != nil {
fmt.Println(err)
return
}

// Example of using zap logger from uber
logger, _ := zap.NewProduction()

sugar := logger.Sugar()
defer sugar.Sync()
defer logger.Sync()

// apply middleware in the same order as you want to execute them
svc = hypercache.ApplyMiddleware(svc,
// middleware.YourMiddleware,
func(next hypercache.Service) hypercache.Service {
return middleware.NewLoggingMiddleware(next, sugar)
},
func(next hypercache.Service) hypercache.Service {
return middleware.NewStatsCollectorMiddleware(next, statsCollector)
},
)

err = svc.Set("key string", "value any", 0)
if err != nil {
fmt.Println(err)
return
}
key, ok := svc.Get("key string")
if !ok {
fmt.Println("key not found")
return
}
fmt.Println(key)
```

## Usage

Here is an example of using the HyperCache package. See the [examples](./examples/README.md) directory for a more comprehensive overview.

```golang
package main

import (
"fmt"
"log"
"time"

"github.com/hyp3rd/hypercache"
)

func main() {
// Create a new HyperCache with a capacity of 10
cache, err := hypercache.NewInMemoryWithDefaults(10)
if err != nil {
fmt.Println(err)
return
}
// Stop the cache when the program exits
defer cache.Stop()

log.Println("adding items to the cache")
// Add 10 items to the cache
for i := 0; i < 10; i++ {
key := fmt.Sprintf("key%d", i)
val := fmt.Sprintf("val%d", i)

err = cache.Set(key, val, time.Minute)

if err != nil {
fmt.Printf("unexpected error: %v\n", err)
return
}
}

log.Println("fetching items from the cache using the `GetMultiple` method, key11 does not exist")
// Retrieve the specific of items from the cache
items, errs := cache.GetMultiple("key1", "key7", "key9", "key11")

// Print the errors if any
for k, e := range errs {
log.Printf("error fetching item %s: %s\n", k, e)
}

// Print the items
for k, v := range items {
fmt.Println(k, v)
}

log.Println("fetching items from the cache using the `GetOrSet` method")
// Retrieve a specific of item from the cache
// If the item is not found, set it and return the value
val, err := cache.GetOrSet("key11", "val11", time.Minute)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(val)

log.Println("fetching items from the cache using the simple `Get` method")
item, ok := cache.Get("key7")
if !ok {
fmt.Println("item not found")
return
}
fmt.Println(item)
}

```
Examples can be too broad for a readme, refer to the [examples](./examples/README.md) directory for a more comprehensive overview.

## License

Expand Down
34 changes: 9 additions & 25 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,31 @@ package backend
import (
"context"

"github.com/hyp3rd/hypercache/models"
"github.com/hyp3rd/hypercache/types"
)

// IBackendConstrain is the interface that defines the constrain type that must be implemented by cache backends.
// IBackendConstrain is the interface that defines the constrain type implemented by cache backends.
type IBackendConstrain interface {
InMemory | Redis
}

// IInMemory is the interface that must be implemented by in-memory cache backends.
type IInMemory[T IBackendConstrain] interface {
// IBackend[T] is the interface that must be implemented by cache backends.
IBackend[T]
// List the items in the cache that meet the specified criteria.
List(options ...FilterOption[InMemory]) ([]*models.Item, error)
// Clear removes all items from the cache.
Clear()
}

// IRedisBackend is the interface that must be implemented by Redis cache backends.
type IRedisBackend[T IBackendConstrain] interface {
// IBackend[T] is the interface that must be implemented by cache backends.
IBackend[T]
// List the items in the cache that meet the specified criteria.
List(ctx context.Context, options ...FilterOption[Redis]) ([]*models.Item, error)
// Clear removes all items from the cache.
Clear() error
}

// IBackend is the interface that must be implemented by cache backends.
type IBackend[T IBackendConstrain] interface {
// Get retrieves the item with the given key from the cache.
// If the key is not found in the cache, it returns nil.
Get(key string) (item *models.Item, ok bool)
Get(key string) (item *types.Item, ok bool)
// Set adds a new item to the cache.
Set(item *models.Item) error
Set(item *types.Item) error
// Capacity returns the maximum number of items that can be stored in the cache.
Capacity() int
// SetCapacity sets the maximum number of items that can be stored in the cache.
SetCapacity(capacity int)
// Count returns the number of items currently stored in the cache.
Count() int
// Remove deletes the item with the given key from the cache.
Remove(keys ...string) error
Remove(ctx context.Context, keys ...string) error
// List the items in the cache that meet the specified criteria.
List(ctx context.Context, filters ...IFilter) ([]*types.Item, error)
// Clear removes all items from the cache.
Clear(ctx context.Context) error
}
86 changes: 86 additions & 0 deletions backend/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package backend

import (
"sort"

"github.com/hyp3rd/hypercache/types"
)

// IFilter is a backend agnostic interface for a filter that can be applied to a list of items.
type IFilter interface {
ApplyFilter(backendType string, items []*types.Item) []*types.Item
}

// sortByFilter is a filter that sorts the items by a given field.
type sortByFilter struct {
field string
}

// ApplyFilter applies the sort filter to the given list of items.
func (f sortByFilter) ApplyFilter(backendType string, items []*types.Item) []*types.Item {
var sorter sort.Interface
switch f.field {
case types.SortByKey.String():
sorter = &itemSorterByKey{items: items}
case types.SortByLastAccess.String():
sorter = &itemSorterByLastAccess{items: items}
case types.SortByAccessCount.String():
sorter = &itemSorterByAccessCount{items: items}
case types.SortByExpiration.String():
sorter = &itemSorterByExpiration{items: items}
default:
return items
}
sort.Sort(sorter)
return items
}

// SortOrderFilter is a filter that sorts the items by a given field.
type SortOrderFilter struct {
ascending bool
}

// ApplyFilter applies the sort order filter to the given list of items.
func (f SortOrderFilter) ApplyFilter(backendType string, items []*types.Item) []*types.Item {
if !f.ascending {
sort.Slice(items, func(i, j int) bool {
return items[j].Key > items[i].Key
})
} else {
sort.Slice(items, func(i, j int) bool {
return items[i].Key < items[j].Key
})
}
return items
}

// filterFunc is a filter that filters the items by a given field's value.
type filterFunc struct {
fn func(item *types.Item) bool
}

// ApplyFilter applies the filter function to the given list of items.
func (f filterFunc) ApplyFilter(backendType string, items []*types.Item) []*types.Item {
filteredItems := make([]*types.Item, 0)
for _, item := range items {
if f.fn(item) {
filteredItems = append(filteredItems, item)
}
}
return filteredItems
}

// WithSortBy returns a filter that sorts the items by a given field.
func WithSortBy(field string) IFilter {
return sortByFilter{field: field}
}

// WithSortOrderAsc returns a filter that determins whether to sort ascending or not.
func WithSortOrderAsc(ascending bool) SortOrderFilter {
return SortOrderFilter{ascending: ascending}
}

// WithFilterFunc returns a filter that filters the items by a given field's value.
func WithFilterFunc(fn func(item *types.Item) bool) IFilter {
return filterFunc{fn: fn}
}
Loading

0 comments on commit abcb48d

Please sign in to comment.