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

Hazelcast store support #198

Merged
merged 1 commit into from
Feb 26, 2023
Merged
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
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ updates:
prefix: "[gomod/go_cache] "
open-pull-requests-limit: 10

- package-ecosystem: gomod
directory: "/store/hazelcast"
schedule:
interval: daily
time: "04:00"
commit-message:
prefix: "[gomod/hazelcast] "
open-pull-requests-limit: 10

- package-ecosystem: gomod
directory: "/store/memcache"
schedule:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
- bigcache
- freecache
- go_cache
- hazelcast
- memcache
- pegasus
- redis
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Here is what it brings in detail:
* [Redis (rueidis)](https://github.com/rueian/rueidis) (rueian/rueidis)
* [Freecache](https://github.com/coocood/freecache) (coocood/freecache)
* [Pegasus](https://pegasus.apache.org/) ([apache/incubator-pegasus](https://github.com/apache/incubator-pegasus)) [benchmark](https://pegasus.apache.org/overview/benchmark/)
* [Hazelcast](https://github.com/hazelcast/hazelcast-go-client) (hazelcast-go-client/hazelcast)
* More to come soon

## Built-in metrics providers
Expand Down Expand Up @@ -242,6 +243,34 @@ if err != nil {
value, _ := cacheManager.Get(ctx, "my-key")
```

#### Hazelcast

```go
client, err := hazelcast.StartNewClient(ctx)
if err != nil {
log.Fatalf("Failed to start client: %v", err)
}

hzMap, err := client.GetMap(ctx, "gocache")
if err != nil {
log.Fatalf("Failed to get map: %v", err)
}

hazelcastStore := hazelcast_store.NewHazelcast(hzMap)

cacheManager := cache.New[string](hazelcastStore)
err := cacheManager.Set("my-key", "my-value", store.WithExpiration(15*time.Second))
if err != nil {
panic(err)
}

value, err := cacheManager.Get(ctx, "my-key")
if err != nil {
panic(err)
}
fmt.Printf("Get the key '%s' from the hazelcast cache. Result: %s", "my-key", value)
```

### A chained cache

Here, we will chain caches in the following order: first in memory with Ristretto store, then in Redis (as a fallback):
Expand Down
26 changes: 26 additions & 0 deletions store/hazelcast/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module github.com/eko/gocache/store/hazelcast/v4

go 1.19

require (
github.com/eko/gocache/lib/v4 v4.1.2
github.com/golang/mock v1.6.0
github.com/hazelcast/hazelcast-go-client v1.3.2
github.com/stretchr/testify v1.8.1
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
)

require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shirou/gopsutil/v3 v3.21.5 // indirect
github.com/tklauser/go-sysconf v0.3.4 // indirect
github.com/tklauser/numcpus v0.2.1 // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/sys v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/eko/gocache/lib/v4 => ../../lib/
65 changes: 65 additions & 0 deletions store/hazelcast/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/apache/thrift v0.14.1 h1:Yh8v0hpCj63p5edXOLaqTJW0IJ1p+eMW6+YSOqw1d6s=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/hazelcast/hazelcast-go-client v1.3.2 h1:RSaEoL5NIoPZuKbHWXcgYO12xZW8Br63DYhMENXVRCo=
github.com/hazelcast/hazelcast-go-client v1.3.2/go.mod h1:JH7sI0kvMSlJJ5D+YFRg/J/P41MRPffzCt0bN9LYd0M=
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/shirou/gopsutil/v3 v3.21.5 h1:YUBf0w/KPLk7w1803AYBnH7BmA+1Z/Q5MEZxpREUaB4=
github.com/shirou/gopsutil/v3 v3.21.5/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tklauser/go-sysconf v0.3.4 h1:HT8SVixZd3IzLdfs/xlpq0jeSfTX57g1v6wB1EuzV7M=
github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek=
github.com/tklauser/numcpus v0.2.1 h1:ct88eFm+Q7m2ZfXJdan1xYoXKlmwsfP+k88q05KvlZc=
github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
145 changes: 145 additions & 0 deletions store/hazelcast/hazelcast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package hazelcast

import (
"context"
"errors"
"fmt"
"strings"
"time"

lib_store "github.com/eko/gocache/lib/v4/store"
"github.com/hazelcast/hazelcast-go-client/types"
"golang.org/x/sync/errgroup"
)

// HazelcastMapInterface represents a hazelcast/hazelcast-go-client map
type HazelcastMapInterface interface {
Get(ctx context.Context, key any) (any, error)
GetEntryView(ctx context.Context, key any) (*types.SimpleEntryView, error)
SetWithTTL(ctx context.Context, key any, value any, ttl time.Duration) error
Remove(ctx context.Context, key any) (any, error)
Clear(ctx context.Context) error
}

const (
// HazelcastType represents the storage type as a string value
HazelcastType = "hazelcast"
// HazelcastTagPattern represents the tag pattern to be used as a key in specified storage
HazelcastTagPattern = "gocache_tag_%s"

TagKeyExpiry = 720 * time.Hour
)

// HazelcastStore is a store for Hazelcast
type HazelcastStore struct {
hzMap HazelcastMapInterface
options *lib_store.Options
}

// NewHazelcast creates a new store to Hazelcast instance(s)
func NewHazelcast(hzMap HazelcastMapInterface, options ...lib_store.Option) *HazelcastStore {
return &HazelcastStore{
hzMap: hzMap,
options: lib_store.ApplyOptions(options...),
}
}

// Get returns data stored from a given key
func (s *HazelcastStore) Get(ctx context.Context, key any) (any, error) {
value, err := s.hzMap.Get(ctx, key)
if err != nil {
return nil, err
}
if value == nil {
return nil, lib_store.NotFoundWithCause(errors.New("unable to retrieve data from hazelcast"))
}
return value, err
}

// GetWithTTL returns data stored from a given key and its corresponding TTL
func (s *HazelcastStore) GetWithTTL(ctx context.Context, key any) (any, time.Duration, error) {
entryView, err := s.hzMap.GetEntryView(ctx, key)
if err != nil {
return nil, 0, err
}
if entryView == nil {
return nil, 0, lib_store.NotFoundWithCause(errors.New("unable to retrieve data from hazelcast"))
}
return entryView.Value, time.Duration(entryView.TTL) * time.Millisecond, err
}

// Set defines data in Hazelcast for given key identifier
func (s *HazelcastStore) Set(ctx context.Context, key any, value any, options ...lib_store.Option) error {
opts := lib_store.ApplyOptionsWithDefault(s.options, options...)
err := s.hzMap.SetWithTTL(ctx, key, value, opts.Expiration)
if err != nil {
return err
}
if tags := opts.Tags; len(tags) > 0 {
s.setTags(ctx, key, tags)
}
return nil
}

func (s *HazelcastStore) setTags(ctx context.Context, key any, tags []string) {
group, ctx := errgroup.WithContext(ctx)
for _, tag := range tags {
currentTag := tag
group.Go(func() error {
tagKey := fmt.Sprintf(HazelcastTagPattern, currentTag)
tagValue, err := s.hzMap.Get(ctx, tagKey)
if err != nil {
return err
}
if tagValue == nil {
return s.hzMap.SetWithTTL(ctx, tagKey, key.(string), TagKeyExpiry)
}
cacheKeys := strings.Split(tagValue.(string), ",")
for _, cacheKey := range cacheKeys {
if key == cacheKey {
return nil
}
}
cacheKeys = append(cacheKeys, key.(string))
newTagValue := strings.Join(cacheKeys, ",")
return s.hzMap.SetWithTTL(ctx, tagKey, newTagValue, TagKeyExpiry)
})
}
group.Wait()
}

// Delete removes data from Hazelcast for given key identifier
func (s *HazelcastStore) Delete(ctx context.Context, key any) error {
_, err := s.hzMap.Remove(ctx, key)
return err
}

// Invalidate invalidates some cache data in Hazelcast for given options
func (s *HazelcastStore) Invalidate(ctx context.Context, options ...lib_store.InvalidateOption) error {
opts := lib_store.ApplyInvalidateOptions(options...)
if tags := opts.Tags; len(tags) > 0 {
for _, tag := range tags {
tagKey := fmt.Sprintf(HazelcastTagPattern, tag)
tagValue, err := s.hzMap.Get(ctx, tagKey)
if err != nil || tagValue == nil {
continue
}
cacheKeys := strings.Split(tagValue.(string), ",")
for _, cacheKey := range cacheKeys {
s.Delete(ctx, cacheKey)
}
s.Delete(ctx, tagKey)
}
}
return nil
}

// Clear resets all data in the store
func (s *HazelcastStore) Clear(ctx context.Context) error {
return s.hzMap.Clear(ctx)
}

// GetType returns the store type
func (s *HazelcastStore) GetType() string {
return HazelcastType
}
68 changes: 68 additions & 0 deletions store/hazelcast/hazelcast_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package hazelcast

import (
"context"
"fmt"
"math"
"testing"

lib_store "github.com/eko/gocache/lib/v4/store"
"github.com/hazelcast/hazelcast-go-client"
)

func BenchmarkHazelcastSet(b *testing.B) {
ctx := context.Background()

client, err := hazelcast.StartNewClient(ctx)
if err != nil {
b.Fatalf("Failed to start client: %v", err)
}

hzMap, err := client.GetMap(ctx, "gocache")
if err != nil {
b.Fatalf("Failed to get map: %v", err)
}

store := NewHazelcast(hzMap)

for k := 0.; k <= 10; k++ {
n := int(math.Pow(2, k))
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
for i := 0; i < b.N*n; i++ {
key := fmt.Sprintf("test-%d", n)
value := []byte(fmt.Sprintf("value-%d", n))
store.Set(ctx, key, value, lib_store.WithTags([]string{fmt.Sprintf("tag-%d", n)}))
}
})
}
}

func BenchmarkHazelcastGet(b *testing.B) {
ctx := context.Background()

client, err := hazelcast.StartNewClient(ctx)
if err != nil {
b.Fatalf("Failed to start client: %v", err)
}

hzMap, err := client.GetMap(ctx, "gocache")
if err != nil {
b.Fatalf("Failed to get map: %v", err)
}

store := NewHazelcast(hzMap)

key := "test"
value := []byte("value")

store.Set(ctx, key, value)

for k := 0.; k <= 10; k++ {
n := int(math.Pow(2, k))
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
for i := 0; i < b.N*n; i++ {
_, _ = store.Get(ctx, key)
}
})
}
}
Loading