diff --git a/backend/backend.go b/backend/backend.go index 4bbf618..a53c4e4 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -27,7 +27,7 @@ type IBackend[T IBackendConstrain] interface { // Remove deletes the item with the given key from the cache. 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) + List(ctx context.Context, filters ...IFilter) (items []*types.Item, err error) // Clear removes all items from the cache. Clear(ctx context.Context) error } diff --git a/backend/filters.go b/backend/filters.go index c1f31f8..c54667f 100644 --- a/backend/filters.go +++ b/backend/filters.go @@ -1,14 +1,25 @@ package backend import ( + "fmt" "sort" "github.com/hyp3rd/hypercache/types" ) +// itemSorter is a custom sorter for the items +type itemSorter struct { + items []*types.Item + less func(i, j *types.Item) bool +} + +func (s *itemSorter) Len() int { return len(s.items) } +func (s *itemSorter) Swap(i, j int) { s.items[i], s.items[j] = s.items[j], s.items[i] } +func (s *itemSorter) Less(i, j int) bool { return s.less(s.items[i], s.items[j]) } + // 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 + ApplyFilter(backendType string, items []*types.Item) ([]*types.Item, error) } // sortByFilter is a filter that sorts the items by a given field. @@ -16,66 +27,22 @@ 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. +// WithSortOrderAsc returns a filter that determines whether to sort ascending or not. func WithSortOrderAsc(ascending bool) SortOrderFilter { return SortOrderFilter{ascending: ascending} } @@ -84,3 +51,65 @@ func WithSortOrderAsc(ascending bool) SortOrderFilter { func WithFilterFunc(fn func(item *types.Item) bool) IFilter { return filterFunc{fn: fn} } + +// ApplyFilter applies the sort by filter to the given list of items. +func (f sortByFilter) ApplyFilter(backendType string, items []*types.Item) ([]*types.Item, error) { + var sorter *itemSorter + + switch f.field { + case types.SortByKey.String(): + sorter = &itemSorter{ + items: items, + less: func(i, j *types.Item) bool { + return i.Key < j.Key + }, + } + case types.SortByLastAccess.String(): + sorter = &itemSorter{ + items: items, + less: func(i, j *types.Item) bool { + return i.LastAccess.UnixNano() < j.LastAccess.UnixNano() + }, + } + case types.SortByAccessCount.String(): + sorter = &itemSorter{ + items: items, + less: func(i, j *types.Item) bool { + return i.AccessCount < j.AccessCount + }, + } + case types.SortByExpiration.String(): + sorter = &itemSorter{ + items: items, + less: func(i, j *types.Item) bool { + return i.Expiration < j.Expiration + }, + } + default: + return nil, fmt.Errorf("invalid sort field: %s", f.field) + } + + sort.Sort(sorter) + return items, nil +} + +// ApplyFilter applies the sort order filter to the given list of items. +func (f SortOrderFilter) ApplyFilter(backendType string, items []*types.Item) ([]*types.Item, error) { + if !f.ascending { + for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 { + items[i], items[j] = items[j], items[i] + } + } + return items, nil +} + +// ApplyFilter applies the filter function to the given list of items. +func (f filterFunc) ApplyFilter(backendType string, items []*types.Item) ([]*types.Item, error) { + filteredItems := make([]*types.Item, 0) + for _, item := range items { + if f.fn(item) { + filteredItems = append(filteredItems, item) + } + } + return filteredItems, nil +} diff --git a/backend/inmemory.go b/backend/inmemory.go index b4466c2..0aee056 100644 --- a/backend/inmemory.go +++ b/backend/inmemory.go @@ -14,7 +14,6 @@ type InMemory struct { items datastructure.ConcurrentMap // map to store the items in the cache capacity int // capacity of the cache, limits the number of items that can be stored in the cache sync.RWMutex // mutex to protect the cache from concurrent access - // SortFilters // filters applied when listing the items in the cache } // NewInMemory creates a new in-memory cache with the given options. @@ -76,12 +75,12 @@ func (cacheBackend *InMemory) Set(item *types.Item) error { } // List returns a list of all items in the cache filtered and ordered by the given options -func (cacheBackend *InMemory) List(ctx context.Context, filters ...IFilter) ([]*types.Item, error) { +func (cacheBackend *InMemory) List(ctx context.Context, filters ...IFilter) (items []*types.Item, err error) { // Apply the filters cacheBackend.RLock() defer cacheBackend.RUnlock() - items := make([]*types.Item, 0, cacheBackend.items.Count()) + items = make([]*types.Item, 0, cacheBackend.items.Count()) for item := range cacheBackend.items.IterBuffered() { copy := item @@ -90,18 +89,13 @@ func (cacheBackend *InMemory) List(ctx context.Context, filters ...IFilter) ([]* // Apply the filters if len(filters) > 0 { - wg := sync.WaitGroup{} - wg.Add(len(filters)) for _, filter := range filters { - go func(filter IFilter) { - defer wg.Done() - items = filter.ApplyFilter("in-memory", items) - }(filter) + items, err = filter.ApplyFilter("in-memory", items) } - wg.Wait() + } - return items, nil + return items, err } // Remove removes items with the given key from the cacheBackend. If an item is not found, it does nothing. diff --git a/backend/redis.go b/backend/redis.go index 4a49df8..a5327cf 100644 --- a/backend/redis.go +++ b/backend/redis.go @@ -2,7 +2,6 @@ package backend import ( "context" - "sync" "github.com/hyp3rd/hypercache/errors" "github.com/hyp3rd/hypercache/libs/serializer" @@ -16,7 +15,6 @@ type Redis struct { capacity int // capacity of the cache, limits the number of items that can be stored in the cache keysSetName string // keysSetName is the name of the set that holds the keys of the items in the cache Serializer serializer.ISerializer // Serializer is the serializer used to serialize the items before storing them in the cache - // SortFilters // SortFilters holds the filters applied when listing the items in the cache } // NewRedis creates a new redis cache with the given options. @@ -143,7 +141,7 @@ func (cacheBackend *Redis) Set(item *types.Item) error { } // List returns a list of all the items in the cacheBackend that match the given filter options. -func (cacheBackend *Redis) List(ctx context.Context, filters ...IFilter) ([]*types.Item, error) { +func (cacheBackend *Redis) List(ctx context.Context, filters ...IFilter) (items []*types.Item, err error) { // Get the set of keys held in the cacheBackend with the given `keysSetName` keys, err := cacheBackend.rdb.SMembers(ctx, cacheBackend.keysSetName).Result() if err != nil { @@ -163,7 +161,7 @@ func (cacheBackend *Redis) List(ctx context.Context, filters ...IFilter) ([]*typ } // Create a slice to hold the items - items := make([]*types.Item, 0, len(keys)) + items = make([]*types.Item, 0, len(keys)) // Deserialize the items and add them to the slice of items to return for _, cmd := range cmds { @@ -179,18 +177,12 @@ func (cacheBackend *Redis) List(ctx context.Context, filters ...IFilter) ([]*typ // Apply the filters if len(filters) > 0 { - wg := sync.WaitGroup{} - wg.Add(len(filters)) for _, filter := range filters { - go func(filter IFilter) { - defer wg.Done() - items = filter.ApplyFilter("redis", items) - }(filter) + items, err = filter.ApplyFilter("redis", items) } - wg.Wait() } - return items, nil + return items, err } // Remove removes an item from the cache with the given key diff --git a/backend/sorting.go b/backend/sorting.go deleted file mode 100644 index 36471be..0000000 --- a/backend/sorting.go +++ /dev/null @@ -1,53 +0,0 @@ -package backend - -import "github.com/hyp3rd/hypercache/types" - -// SortFilters holds the filters applied when listing the items in the cache -type SortFilters struct { - // SortBy is the field to sort the items by. - // The field can be any of the fields in the `Item` struct. - SortBy string - // SortAscending is a boolean indicating whether the items should be sorted in ascending order. - // If set to false, the items will be sorted in descending order. - SortAscending bool - // FilterFunc is a predicate that takes a `Item` as an argument and returns a boolean indicating whether the item should be included in the cache. - // FilterFunc func(item *types.Item) bool // filters applied when listing the items in the cache -} - -type itemSorterByKey struct { - items []*types.Item -} - -func (s *itemSorterByKey) Len() int { return len(s.items) } -func (s *itemSorterByKey) Swap(i, j int) { s.items[i], s.items[j] = s.items[j], s.items[i] } -func (s *itemSorterByKey) Less(i, j int) bool { return s.items[i].Key < s.items[j].Key } - -type itemSorterByExpiration struct { - items []*types.Item -} - -func (s *itemSorterByExpiration) Len() int { return len(s.items) } -func (s *itemSorterByExpiration) Swap(i, j int) { s.items[i], s.items[j] = s.items[j], s.items[i] } -func (s *itemSorterByExpiration) Less(i, j int) bool { - return s.items[i].Expiration < s.items[j].Expiration -} - -type itemSorterByLastAccess struct { - items []*types.Item -} - -func (s *itemSorterByLastAccess) Len() int { return len(s.items) } -func (s *itemSorterByLastAccess) Swap(i, j int) { s.items[i], s.items[j] = s.items[j], s.items[i] } -func (s *itemSorterByLastAccess) Less(i, j int) bool { - return s.items[i].LastAccess.UnixNano() < s.items[j].LastAccess.UnixNano() -} - -type itemSorterByAccessCount struct { - items []*types.Item -} - -func (s *itemSorterByAccessCount) Len() int { return len(s.items) } -func (s *itemSorterByAccessCount) Swap(i, j int) { s.items[i], s.items[j] = s.items[j], s.items[i] } -func (s *itemSorterByAccessCount) Less(i, j int) bool { - return s.items[i].AccessCount < s.items[j].AccessCount -} diff --git a/examples/eviction/eviction.go b/examples/eviction/eviction.go index a87e488..97578cf 100644 --- a/examples/eviction/eviction.go +++ b/examples/eviction/eviction.go @@ -60,20 +60,16 @@ func executeExample(evictionInterval time.Duration) { log.Println("capacity after adding 15 items", cache.Capacity()) log.Println("listing all items in the cache") - items, err := cache.List(context.TODO()) + + // Apply filters + sortByFilter := backend.WithSortBy(types.SortByKey.String()) + items, err := cache.List(context.TODO(), sortByFilter) if err != nil { fmt.Println(err) return } - // Apply filters - sortByFilter := backend.WithSortBy(types.SortByKey.String()) - sortOrderFilter := backend.WithSortOrderAsc(true) - - filteredItems := sortByFilter.ApplyFilter("in-memory", items) - sortedItems := sortOrderFilter.ApplyFilter("in-memory", filteredItems) - - for _, item := range sortedItems { + for _, item := range items { fmt.Println(item.Key, item.Value) } diff --git a/examples/list/list.go b/examples/list/list.go index 99c75a4..1f94b66 100644 --- a/examples/list/list.go +++ b/examples/list/list.go @@ -43,13 +43,14 @@ func main() { } sortByFilter := backend.WithSortBy(types.SortByExpiration.String()) - sortOrderFilter := backend.WithSortOrderAsc(true) + + sortOrder := backend.WithSortOrderAsc(true) // Create a filterFuncFilter with the defined filter function filter := backend.WithFilterFunc(itemsFilterFunc) // Retrieve the list of items from the cache - items, err := hyperCache.List(context.TODO(), sortByFilter, sortOrderFilter, filter) + items, err := hyperCache.List(context.TODO(), sortByFilter, sortOrder, filter) if err != nil { fmt.Println(err) return diff --git a/examples/redis/redis.go b/examples/redis/redis.go index 8330e86..b45930f 100644 --- a/examples/redis/redis.go +++ b/examples/redis/redis.go @@ -51,13 +51,6 @@ func main() { fmt.Println("fetching all items (sorted by key, ascending, filtered by value != 'value-16')") - // Retrieve the list of items from the cache - allItems, err := hyperCache.List(context.TODO()) - if err != nil { - fmt.Println(err) - return - } - // Apply filters // Define a filter function itemsFilterFunc := func(item *types.Item) bool { @@ -71,15 +64,16 @@ func main() { // Create a filterFuncFilter with the defined filter function filter := backend.WithFilterFunc(itemsFilterFunc) - // Apply the filter to the items - filteredItems := filter.ApplyFilter("redis", allItems) - - // Apply the sort filter to the filtered items - filteredItems = sortByFilter.ApplyFilter("redis", filteredItems) + // Retrieve the list of items from the cache + allItems, err := hyperCache.List(context.TODO(), sortByFilter, filter) + if err != nil { + fmt.Println(err) + return + } fmt.Println("printing all items") // Print the list of items - for _, item := range filteredItems { + for _, item := range allItems { fmt.Println(item.Key, item.Value) } diff --git a/hypercache.go b/hypercache.go index 623fdd1..e567b0d 100644 --- a/hypercache.go +++ b/hypercache.go @@ -233,21 +233,18 @@ func (hyperCache *HyperCache[T]) expirationLoop(ctx context.Context) { ) // get all expired items - items, err = hyperCache.List(context.TODO()) + items, err = hyperCache.List(context.TODO(), + backend.WithSortBy(types.SortByExpiration.String()), + backend.WithFilterFunc(func(item *types.Item) bool { + return item.Expiration > 0 && time.Since(item.LastAccess) > item.Expiration + })) + if err != nil { return err } - sortByFilter := backend.WithSortBy(types.SortByExpiration.String()) - filterFuncFilter := backend.WithFilterFunc(func(item *types.Item) bool { - return item.Expiration > 0 && time.Since(item.LastAccess) > item.Expiration - }) - - filteredItems := filterFuncFilter.ApplyFilter(hyperCache.cacheBackendChecker.GetRegisteredType(), items) - sortedItems := sortByFilter.ApplyFilter(hyperCache.cacheBackendChecker.GetRegisteredType(), filteredItems) - // iterate all expired items and remove them - for _, item := range sortedItems { + for _, item := range items { expiredCount++ hyperCache.Remove(ctx, item.Key) types.ItemPool.Put(item)