Skip to content

Commit

Permalink
Merge pull request #16 from Sovietaced/logging
Browse files Browse the repository at this point in the history
Add support for redis backed map
  • Loading branch information
Sovietaced authored Dec 15, 2023
2 parents 768b93b + 0eda81b commit b9ed09d
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 30 deletions.
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,29 @@ Distributed data structures backed by Redis. Heavily inspired by [Redisson](http
The `Mutex` struct aims to provide identical semantics to the [sync.Mutex](https://pkg.go.dev/sync#Mutex) package.

```go
import (
"github.com/redis/go-redis/v9"
"github.com/sovietaced/go-redisson/mutex"
)

func main() {
client := redis.NewClient(&redis.Options{Addr: endpoint})
mutex := mutex.NewMutex(client, "test")
err := mutex.Lock(ctx)
err = mutex.Unlock(ctx)
}
ctx := context.Background()
client := redis.NewClient(&redis.Options{Addr: endpoint})
mutex := mutex.NewMutex(client, "test")
err := mutex.Lock(ctx)
err = mutex.Unlock(ctx)
```

### Distributed Map

The `Mapp` struct aims to provide similar semantics to native Go map.

```go
ctx := context.Background()
client := redis.NewClient(&redis.Options{Addr: endpoint})
m := mapp.NewMapp(client)
err = m.Set(ctx, "key", "value")
value, exists, err := m.Get(ctx, "key")
```

The `Mapp` struct supports generics so you can use any struct you'd like for the key/value. By default, structs will be
marshalled using json but can be configured with key/value marshalers.

```go

m := mapp.NewMapp(client, mapp.WithKeyMarshaler(...), mapp.WithValueMarshaller(...))
```
126 changes: 126 additions & 0 deletions mapp/mapp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package mapp

import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"github.com/sovietaced/go-redisson/marshal"
)

type Options[K any, V any] struct {
namespace string
keyMarshaler marshal.Marshaler[K]
valueMarshaler marshal.Marshaler[V]
}

func defaultOptions[K any, V any]() *Options[K, V] {
opts := &Options[K, V]{}
WithKeyMarshaler[K, V](&marshal.JsonMarshaler[K]{})(opts)
WithValueMarshaler[K, V](&marshal.JsonMarshaler[V]{})(opts)
return opts
}

type Option[K any, V any] func(*Options[K, V])

func WithNamespace[K any, V any](namespace string) Option[K, V] {
return func(mo *Options[K, V]) {
mo.namespace = namespace
}
}

func WithKeyMarshaler[K any, V any](marshaler marshal.Marshaler[K]) Option[K, V] {
return func(mo *Options[K, V]) {
mo.keyMarshaler = marshaler
}
}

func WithValueMarshaler[K any, V any](marshaler marshal.Marshaler[V]) Option[K, V] {
return func(mo *Options[K, V]) {
mo.valueMarshaler = marshaler
}
}

type Mapp[K any, V any] struct {
namespace string
client redis.UniversalClient
keyMarshaler marshal.Marshaler[K]
valueMarshaler marshal.Marshaler[V]
}

func NewMapp[K any, V any](client redis.UniversalClient, options ...Option[K, V]) *Mapp[K, V] {
opts := defaultOptions[K, V]()
for _, option := range options {
option(opts)
}

return &Mapp[K, V]{client: client, keyMarshaler: opts.keyMarshaler, valueMarshaler: opts.valueMarshaler, namespace: opts.namespace}
}

func (c *Mapp[K, V]) Get(ctx context.Context, key K) (V, bool, error) {
keyString, err := c.computeKey(ctx, key)
if err != nil {
return *new(V), false, fmt.Errorf("computing key: %w", err)
}

result := c.client.Get(ctx, keyString)
if result.Err() != nil {
// Missing key
if result.Err() == redis.Nil {
return *new(V), false, nil
}
return *new(V), false, fmt.Errorf("getting value: %w", err)
}

value := new(V)
if err = c.valueMarshaler.Unmarshal(ctx, result.Val(), value); err != nil {
return *value, true, fmt.Errorf("unmarshalling value: %w", err)
}

return *value, true, nil
}

func (c *Mapp[K, V]) Set(ctx context.Context, key K, value V) error {
keyString, err := c.computeKey(ctx, key)
if err != nil {
return fmt.Errorf("computing key: %w", err)
}

marshaledValue, err := c.valueMarshaler.Marshal(ctx, value)
if err != nil {
return fmt.Errorf("marshalling value: %w", err)
}

result := c.client.Set(ctx, keyString, marshaledValue, 0)
if result.Err() != nil {
return fmt.Errorf("setting key=%s: %w", keyString, err)
}

return nil
}

func (c *Mapp[K, V]) Del(ctx context.Context, key K) error {
keyString, err := c.computeKey(ctx, key)
if err != nil {
return fmt.Errorf("computing key: %w", err)
}

result := c.client.Del(ctx, keyString)
if result.Err() != nil {
return fmt.Errorf("deleting key=%s: %w", keyString, err)
}

return nil
}

func (c *Mapp[K, V]) computeKey(ctx context.Context, key K) (string, error) {
marshaledKey, err := c.keyMarshaler.Marshal(ctx, key)
if err != nil {
return "", fmt.Errorf("marshalling key: %w", err)
}

if len(c.namespace) > 0 {
return fmt.Sprintf("%s:%s", c.namespace, marshaledKey), nil
}

return marshaledKey, nil
}
71 changes: 71 additions & 0 deletions mapp/mapp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package mapp

import (
"context"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"math/rand"
"testing"
)

func TestCache(t *testing.T) {

ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "redis:latest",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
}
redisContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("failed to create redis container: %v", err)
}
defer func() {
if err := redisContainer.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err.Error())
}
}()

endpoint, err := redisContainer.Endpoint(ctx, "")
if err != nil {
t.Fatalf("failed to get container endpoint: %v", err)
}

client := redis.NewClient(&redis.Options{Addr: endpoint})

t.Run("get, set, delete string key/value", func(t *testing.T) {
cache := NewMapp[string, string](client, WithNamespace[string, string](RandomNamespace()))

err = cache.Set(ctx, "key", "value")
require.NoError(t, err)

value, exists, err := cache.Get(ctx, "key")
require.NoError(t, err)
require.True(t, exists)
require.Equal(t, "value", value)

err = cache.Del(ctx, "key")
require.NoError(t, err)

value, exists, err = cache.Get(ctx, "key")
require.NoError(t, err)
require.False(t, exists)
require.Equal(t, "", value)
})

}

func RandomNamespace() string {
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

b := make([]rune, 20)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
22 changes: 22 additions & 0 deletions marshal/json_marshaler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package marshal

import (
"context"
"encoding/json"
)

type JsonMarshaler[T any] struct {
}

func (jm *JsonMarshaler[T]) Marshal(ctx context.Context, value T) (string, error) {
bytes, err := json.Marshal(value)
if err != nil {
return "", err
}

return string(bytes), nil
}

func (jm *JsonMarshaler[T]) Unmarshal(ctx context.Context, valueString string, value *T) error {
return json.Unmarshal([]byte(valueString), value)
}
26 changes: 26 additions & 0 deletions marshal/json_marshaler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package marshal

import (
"context"
"github.com/stretchr/testify/require"
"testing"
)

func TestJsonMarshaler(t *testing.T) {

ctx := context.Background()

t.Run("test marshal and unmarshal", func(t *testing.T) {
marshaler := JsonMarshaler[string]{}

marshalled, err := marshaler.Marshal(ctx, "test")
require.NoError(t, err)
require.Equal(t, "\"test\"", marshalled)

var unmarshalled string
err = marshaler.Unmarshal(ctx, marshalled, &unmarshalled)
require.NoError(t, err)

require.Equal(t, "test", unmarshalled)
})
}
10 changes: 10 additions & 0 deletions marshal/marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package marshal

import (
"context"
)

type Marshaler[T any] interface {
Marshal(ctx context.Context, value T) (string, error)
Unmarshal(ctx context.Context, valueString string, value *T) error
}
2 changes: 1 addition & 1 deletion mutex/mutex.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func defaultOptions() *Options {

type Option func(*Options)

// WithLeaseDuration specifies the TTL on the underlying Redis cache entry. Practically speaking, this is the upper
// WithLeaseDuration specifies the TTL on the underlying Redis map entry. Practically speaking, this is the upper
// bound on how long a lock will appear to be held when its owner abandons it.
func WithLeaseDuration(leaseDuration time.Duration) Option {
return func(mo *Options) {
Expand Down
18 changes: 0 additions & 18 deletions redisson.go

This file was deleted.

0 comments on commit b9ed09d

Please sign in to comment.