diff --git a/storage/redis/client.go b/storage/redis/client.go new file mode 100644 index 000000000..2f6cfc095 --- /dev/null +++ b/storage/redis/client.go @@ -0,0 +1,53 @@ +package redis + +import ( + "time" + + "github.com/go-redis/redis" +) + +// Client defines the interface for communicating with a Storj redis instance +type Client interface { + Get(key string) ([]byte, error) + Set(key string, value []byte, ttl time.Duration) error + Ping() error +} + +// Client is the entrypoint into Redis +type redisClient struct { + DB *redis.Client +} + +// NewRedisClient returns a configured Client instance, verifying a sucessful connection to redis +func NewRedisClient(address, password string, db int) (Client, error) { + c := &redisClient{ + DB: redis.NewClient(&redis.Options{ + Addr: address, + Password: password, + DB: db, + }), + } + + // ping here to verify we are able to connect to the redis instacne with the initialized client. + if err := c.DB.Ping().Err(); err != nil { + return nil, err + } + + return c, nil +} + +// Get looks up the provided key from the redis cache returning either an error or the result. +func (c *redisClient) Get(key string) ([]byte, error) { + return c.DB.Get(key).Bytes() +} + +// Set adds a value to the provided key in the Redis cache, returning an error on failure. + +func (c *redisClient) Set(key string, value []byte, ttl time.Duration) error { + return c.DB.Set(key, value, ttl).Err() +} + +// Ping returns an error if pinging the underlying redis server failed +func (c *redisClient) Ping() error { + return c.DB.Ping().Err() +} diff --git a/storage/redis/mock_client.go b/storage/redis/mock_client.go new file mode 100644 index 000000000..0bb177818 --- /dev/null +++ b/storage/redis/mock_client.go @@ -0,0 +1,49 @@ +package redis + +import ( + "errors" + "time" +) + +type mockRedisClient struct { + data map[string][]byte + getCalled int + setCalled int + pingCalled int +} + +var ErrMissingKey = errors.New("missing") +var ErrForced = errors.New("error forced by using 'error' key in mock") + +func (m *mockRedisClient) Get(key string) ([]byte, error) { + m.getCalled++ + if key == "error" { + return []byte{}, ErrForced + } + v, ok := m.data[key] + if !ok { + return []byte{}, ErrMissingKey + } + + return v, nil +} + +func (m *mockRedisClient) Set(key string, value []byte, ttl time.Duration) error { + m.setCalled++ + m.data[key] = value + return nil +} + +func (m *mockRedisClient) Ping() error { + m.pingCalled++ + return nil +} + +func newMockRedisClient(d map[string][]byte) *mockRedisClient { + return &mockRedisClient{ + data: d, + getCalled: 0, + setCalled: 0, + pingCalled: 0, + } +} diff --git a/storage/redis/overlay.go b/storage/redis/overlay.go new file mode 100644 index 000000000..99e84e0bc --- /dev/null +++ b/storage/redis/overlay.go @@ -0,0 +1,57 @@ +package redis + +import ( + "time" + + "github.com/gogo/protobuf/proto" + + "storj.io/storj/protos/overlay" +) + +const defaultNodeExpiration = 61 * time.Minute + +// OverlayClient is used to store overlay data in Redis +type OverlayClient struct { + DB Client +} + +// NewOverlayClient returns a pointer to a new OverlayClient instance with an initalized connection to Redis. +func NewOverlayClient(address, password string, db int) (*OverlayClient, error) { + rc, err := NewRedisClient(address, password, db) + if err != nil { + return nil, err + } + + o := &OverlayClient{ + DB: rc, + } + + // ping here to verify we are able to connect to the redis instacne with the initialized client. + if err := o.DB.Ping(); err != nil { + return nil, err + } + + return o, nil +} + +// Get looks up the provided nodeID from the redis cache +func (o *OverlayClient) Get(key string) (*overlay.NodeAddress, error) { + d, err := o.DB.Get(key) + if err != nil { + return nil, err + } + + na := &overlay.NodeAddress{} + + return na, proto.Unmarshal(d, na) +} + +// Set adds a nodeID to the redis cache with a binary representation of proto defined NodeAddress +func (o *OverlayClient) Set(nodeID string, value overlay.NodeAddress) error { + data, err := proto.Marshal(&value) + if err != nil { + return err + } + + return o.DB.Set(nodeID, data, defaultNodeExpiration) +} diff --git a/storage/redis/overlay_test.go b/storage/redis/overlay_test.go new file mode 100644 index 000000000..443576728 --- /dev/null +++ b/storage/redis/overlay_test.go @@ -0,0 +1,113 @@ +package redis + +import ( + "testing" + + "github.com/coyle/storj/protos/overlay" + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/assert" +) + +func TestGet(t *testing.T) { + cases := []struct { + testID string + expectedTimesCalled int + key string + expectedResponse *overlay.NodeAddress + expectedError error + client *mockRedisClient + }{ + { + testID: "valid Get", + expectedTimesCalled: 1, + key: "foo", + expectedResponse: &overlay.NodeAddress{Transport: overlay.NodeTransport_TCP, Address: "127.0.0.1:9999"}, + expectedError: nil, + client: newMockRedisClient(map[string][]byte{"foo": func() []byte { + na := &overlay.NodeAddress{Transport: overlay.NodeTransport_TCP, Address: "127.0.0.1:9999"} + d, err := proto.Marshal(na) + assert.NoError(t, err) + return d + }()}), + }, + { + testID: "error Get from redis", + expectedTimesCalled: 1, + key: "error", + expectedResponse: nil, + expectedError: ErrForced, + client: newMockRedisClient(map[string][]byte{"error": func() []byte { + na := &overlay.NodeAddress{Transport: overlay.NodeTransport_TCP, Address: "127.0.0.1:9999"} + d, err := proto.Marshal(na) + assert.NoError(t, err) + return d + }()}), + }, + { + testID: "get missing key", + expectedTimesCalled: 1, + key: "bar", + expectedResponse: nil, + expectedError: ErrMissingKey, + client: newMockRedisClient(map[string][]byte{"foo": func() []byte { + na := &overlay.NodeAddress{Transport: overlay.NodeTransport_TCP, Address: "127.0.0.1:9999"} + d, err := proto.Marshal(na) + assert.NoError(t, err) + return d + }()}), + }, + } + + for _, c := range cases { + t.Run(c.testID, func(t *testing.T) { + + oc := OverlayClient{DB: c.client} + + assert.Equal(t, 0, c.client.getCalled) + + resp, err := oc.Get(c.key) + assert.Equal(t, c.expectedError, err) + assert.Equal(t, c.expectedResponse, resp) + assert.Equal(t, c.expectedTimesCalled, c.client.getCalled) + }) + } +} + +func TestSet(t *testing.T) { + cases := []struct { + testID string + expectedTimesCalled int + key string + value overlay.NodeAddress + expectedError error + client *mockRedisClient + }{ + { + testID: "valid Set", + expectedTimesCalled: 1, + key: "foo", + value: overlay.NodeAddress{Transport: overlay.NodeTransport_TCP, Address: "127.0.0.1:9999"}, + expectedError: nil, + client: newMockRedisClient(map[string][]byte{}), + }, + } + + for _, c := range cases { + t.Run(c.testID, func(t *testing.T) { + + oc := OverlayClient{DB: c.client} + + assert.Equal(t, 0, c.client.setCalled) + + err := oc.Set(c.key, c.value) + assert.Equal(t, c.expectedError, err) + assert.Equal(t, c.expectedTimesCalled, c.client.setCalled) + + v := c.client.data[c.key] + na := &overlay.NodeAddress{} + + assert.NoError(t, proto.Unmarshal(v, na)) + assert.Equal(t, na, &c.value) + }) + } +}