Repository: googleforgames/open-saves
Pull request #258 (2021-05-13 21:38:38)

refactor: Introduce Cacheable interface

<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://github.com/googleforgames/open-saves/blob/main/docs/contributing.md and developer guide https://github.com/googleforgames/open-saves/blob/main/docs/development.md
2. Please label this pull request according to what type of issue you are addressing.
3. Ensure you have added or ran the appropriate tests for your PR.
-->

**What type of PR is this?**
/kind refactor

**What this PR does / Why we need it**:
Introduce a new Cacheable interface and make encoding and decoding a responsibility of each package (such as Record).
This patch also changes Cache to a struct and moved some encoding and decoding codes into the class. This will make it easier to support caching for additional classes.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Closes #<issue number>`, or `Closes (paste link of issue)`.
-->
Closes #

**Special notes for your reviewer**:


yuryu: ping

Patch:
diff --git a/Makefile b/Makefile
index 336be9c..261bc14 100644
--- a/Makefile
+++ b/Makefile
@@ -16,12 +16,17 @@ BIN_DIR = build
 API_DIR = api
 PROTOC = protoc
 
+CACHE_MOCKED = internal/pkg/cache/cache.go
+CACHE_MOCK = internal/pkg/cache/mock/mock_cache.go
+
+MOCKS_GENERATED = ${CACHE_MOCK}
+
 SERVER_BIN = ${BIN_DIR}/server
 COLLECTOR_BIN = ${BIN_DIR}/collector
 OPENSAVES_GO_PROTOS = ${API_DIR}/open_saves.pb.go ${API_DIR}/open_saves_grpc.pb.go
-ALL_TARGETS = ${SERVER_BIN} ${COLLECTOR_BIN} ${OPENSAVES_GO_PROTOS}
+ALL_TARGETS = ${SERVER_BIN} ${COLLECTOR_BIN} ${OPENSAVES_GO_PROTOS} ${MOCKS_GENERATED}
 
-.PHONY: all clean test server protos swagger FORCE
+.PHONY: all clean test server protos mocks FORCE
 
 all: server collector
 
@@ -29,18 +34,23 @@ server: ${SERVER_BIN}
 
 collector: ${COLLECTOR_BIN}
 
-${SERVER_BIN}: cmd/server/main.go protos FORCE
+mocks: ${MOCKS_GENERATED}
+
+${SERVER_BIN}: cmd/server/main.go protos mocks FORCE
 	go build -o $@ $<
 
-${COLLECTOR_BIN}: cmd/collector/main.go protos FORCE
+${COLLECTOR_BIN}: cmd/collector/main.go protos mocks FORCE
 	go build -o $@ $<
 
+${CACHE_MOCK}: ${CACHE_MOCKED}
+	mockgen -source "$<" -destination "$@"
+
 clean:
 	rm -f ${ALL_TARGETS}
 
-test: server
+test: all
 	go test -race -v ./...
-
+	
 protos: ${OPENSAVES_GO_PROTOS}
 
 ${OPENSAVES_GO_PROTOS}: ${API_DIR}/open_saves.proto
diff --git a/internal/app/collector/collector.go b/internal/app/collector/collector.go
index e17fa1f..b62c0bb 100644
--- a/internal/app/collector/collector.go
+++ b/internal/app/collector/collector.go
@@ -40,7 +40,7 @@ type Config struct {
 
 // Collector is a garbage collector of unused resources in Datastore.
 type Collector struct {
-	cache  cache.Cache
+	cache  *cache.Cache
 	metaDB *metadb.MetaDB
 	blob   blob.BlobStore
 	cfg    *Config
@@ -62,7 +62,7 @@ func newCollector(ctx context.Context, cfg *Config) (*Collector, error) {
 			log.Fatalf("Failed to create a MetaDB instance: %v", err)
 			return nil, err
 		}
-		redis := redis.NewRedis(cfg.Cache)
+		redis := cache.New(redis.NewRedis(cfg.Cache))
 		c := &Collector{
 			blob:   gcs,
 			metaDB: metadb,
diff --git a/internal/app/server/open_saves.go b/internal/app/server/open_saves.go
index 9a34f62..8e18ad9 100644
--- a/internal/app/server/open_saves.go
+++ b/internal/app/server/open_saves.go
@@ -47,7 +47,7 @@ type openSavesServer struct {
 	cloud      string
 	blobStore  blob.BlobStore
 	metaDB     *metadb.MetaDB
-	cacheStore cache.Cache
+	cacheStore *cache.Cache
 
 	pb.UnimplementedOpenSavesServer
 }
@@ -72,7 +72,7 @@ func newOpenSavesServer(ctx context.Context, cloud, project, bucket, cacheAddr s
 			log.Fatalf("Failed to create a MetaDB instance: %v", err)
 			return nil, err
 		}
-		redis := redis.NewRedis(cacheAddr)
+		redis := cache.New(redis.NewRedis(cacheAddr))
 		server := &openSavesServer{
 			cloud:      cloud,
 			blobStore:  gcs,
@@ -102,7 +102,7 @@ func (s *openSavesServer) CreateStore(ctx context.Context, req *pb.CreateStoreRe
 }
 
 func (s *openSavesServer) CreateRecord(ctx context.Context, req *pb.CreateRecordRequest) (*pb.Record, error) {
-	record := record.FromProto(req.Record)
+	record := record.FromProto(req.GetStoreKey(), req.GetRecord())
 	if err := checkRecord(record); err != nil {
 		return nil, err
 	}
@@ -114,8 +114,7 @@ func (s *openSavesServer) CreateRecord(ctx context.Context, req *pb.CreateRecord
 	}
 
 	if shouldCache(req.Hint) {
-		k := cache.FormatKey(req.GetStoreKey(), req.GetRecord().GetKey())
-		s.storeRecordInCache(ctx, k, newRecord)
+		s.storeRecordInCache(ctx, newRecord.CacheKey(), newRecord)
 	}
 	return newRecord.ToProto(), nil
 }
@@ -131,7 +130,7 @@ func (s *openSavesServer) DeleteRecord(ctx context.Context, req *pb.DeleteRecord
 		req.GetStoreKey(), req.GetKey())
 
 	// Purge record from cache store.
-	k := cache.FormatKey(req.GetStoreKey(), req.GetKey())
+	k := record.CacheKey(req.GetStoreKey(), req.GetKey())
 	if err := s.cacheStore.Delete(ctx, k); err != nil {
 		log.Errorf("failed to purge cache for key (%s): %v", k, err)
 	}
@@ -172,7 +171,7 @@ func (s *openSavesServer) DeleteStore(ctx context.Context, req *pb.DeleteStoreRe
 }
 
 func (s *openSavesServer) GetRecord(ctx context.Context, req *pb.GetRecordRequest) (*pb.Record, error) {
-	k := cache.FormatKey(req.GetStoreKey(), req.GetKey())
+	k := record.CacheKey(req.GetStoreKey(), req.GetKey())
 
 	if shouldCheckCache(req.Hint) {
 		r, err := s.getRecordFromCache(ctx, k)
@@ -200,7 +199,7 @@ func (s *openSavesServer) GetRecord(ctx context.Context, req *pb.GetRecordReques
 }
 
 func (s *openSavesServer) UpdateRecord(ctx context.Context, req *pb.UpdateRecordRequest) (*pb.Record, error) {
-	updateTo := record.FromProto(req.GetRecord())
+	updateTo := record.FromProto(req.GetStoreKey(), req.GetRecord())
 	if err := checkRecord(updateTo); err != nil {
 		return nil, err
 	}
@@ -222,8 +221,7 @@ func (s *openSavesServer) UpdateRecord(ctx context.Context, req *pb.UpdateRecord
 
 	// Update cache store.
 	if shouldCache(req.Hint) {
-		k := cache.FormatKey(req.GetStoreKey(), req.GetRecord().GetKey())
-		s.storeRecordInCache(ctx, k, newRecord)
+		s.storeRecordInCache(ctx, newRecord.CacheKey(), newRecord)
 	}
 
 	return newRecord.ToProto(), nil
@@ -279,7 +277,7 @@ func (s *openSavesServer) insertInlineBlob(ctx context.Context, stream pb.OpenSa
 	if err != nil {
 		return nil
 	}
-	cacheKey := cache.FormatKey(meta.GetStoreKey(), meta.GetRecordKey())
+	cacheKey := record.CacheKey()
 	if shouldCache(meta.Hint) {
 		s.storeRecordInCache(ctx, cacheKey, record)
 	} else {
@@ -362,7 +360,7 @@ func (s *openSavesServer) insertExternalBlob(ctx context.Context, stream pb.Open
 		// Do not delete the blob object here. Leave it to the garbage collector.
 		return err
 	}
-	cacheKey := cache.FormatKey(meta.GetStoreKey(), meta.GetRecordKey())
+	cacheKey := record.CacheKey()
 	if shouldCache(meta.Hint) {
 		s.storeRecordInCache(ctx, cacheKey, record)
 	} else {
@@ -443,46 +441,46 @@ func (s *openSavesServer) getExternalBlob(ctx context.Context, req *pb.GetBlobRe
 func (s *openSavesServer) GetBlob(req *pb.GetBlobRequest, stream pb.OpenSaves_GetBlobServer) error {
 	ctx := stream.Context()
 
-	var record *record.Record
+	var rr *record.Record
 	var err error
 	if shouldCheckCache(req.Hint) {
-		record, _ = s.getRecordFromCache(ctx, cache.FormatKey(req.GetStoreKey(), req.GetRecordKey()))
+		rr, _ = s.getRecordFromCache(ctx, record.CacheKey(req.GetStoreKey(), req.GetRecordKey()))
 	}
-	if record != nil {
-		record, err = s.metaDB.GetRecord(ctx, req.GetStoreKey(), req.GetRecordKey())
+	if rr != nil {
+		rr, err = s.metaDB.GetRecord(ctx, req.GetStoreKey(), req.GetRecordKey())
 		if err != nil {
 			log.Errorf("GetBlob: GetRecord failed for store (%v), record (%v): %v",
 				req.GetStoreKey(), req.GetRecordKey(), err)
 		}
 		if shouldCache(req.Hint) {
-			s.storeRecordInCache(ctx, cache.FormatKey(req.GetStoreKey(), req.GetRecordKey()), record)
+			s.storeRecordInCache(ctx, record.CacheKey(req.GetStoreKey(), req.GetRecordKey()), rr)
 		}
 	}
 	meta := &pb.BlobMetadata{
 		StoreKey:  req.GetStoreKey(),
-		RecordKey: record.Key,
-		Size:      record.BlobSize,
+		RecordKey: rr.Key,
+		Size:      rr.BlobSize,
 	}
 	stream.Send(&pb.GetBlobResponse{Response: &pb.GetBlobResponse_Metadata{Metadata: meta}})
-	if record.ExternalBlob != uuid.Nil {
-		return s.getExternalBlob(ctx, req, stream, record)
+	if rr.ExternalBlob != uuid.Nil {
+		return s.getExternalBlob(ctx, req, stream, rr)
 	}
-	err = stream.Send(&pb.GetBlobResponse{Response: &pb.GetBlobResponse_Content{Content: record.Blob}})
+	err = stream.Send(&pb.GetBlobResponse{Response: &pb.GetBlobResponse_Content{Content: rr.Blob}})
 	if err != nil {
-		log.Errorf("GetBlob: Stream send error for store (%v), record (%v): %v", req.GetRecordKey(), record.Key, err)
+		log.Errorf("GetBlob: Stream send error for store (%v), record (%v): %v", req.GetRecordKey(), rr.Key, err)
 	}
 	return err
 }
 
 func (s *openSavesServer) DeleteBlob(ctx context.Context, req *pb.DeleteBlobRequest) (*empty.Empty, error) {
-	record, _, err := s.metaDB.RemoveBlobFromRecord(ctx, req.GetStoreKey(), req.GetRecordKey())
+	rr, _, err := s.metaDB.RemoveBlobFromRecord(ctx, req.GetStoreKey(), req.GetRecordKey())
 	if err != nil {
 		log.Errorf("DeleteBlob: RemoveBlobFromRecord failed, store = %v, record = %v: %v",
 			req.GetStoreKey(), req.GetRecordKey(), err)
 	} else {
-		k := cache.FormatKey(req.GetStoreKey(), req.GetRecordKey())
+		k := rr.CacheKey()
 		if shouldCache(req.Hint) {
-			s.storeRecordInCache(ctx, k, record)
+			s.storeRecordInCache(ctx, k, rr)
 		} else {
 			if err := s.cacheStore.Delete(ctx, k); err != nil {
 				log.Errorf("failed to purge cache for key (%s): %v", k, err)
@@ -499,34 +497,24 @@ func (s *openSavesServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.Pi
 }
 
 func (s *openSavesServer) getRecordFromCache(ctx context.Context, key string) (*record.Record, error) {
-	r, err := s.cacheStore.Get(ctx, key)
+	var r = new(record.Record)
+	err := s.cacheStore.Get(ctx, key, r)
 	if err != nil {
 		// cache miss.
 		return nil, err
 	}
 	// cache hit, use value from cache store.
-	record, err := cache.DecodeRecord(r)
-	if err != nil {
-		return nil, err
-	}
-	log.Debugf("cache hit: %+v", record)
-	return record, nil
+	log.Debugf("cache hit: %+v", r)
+	return r, nil
 }
 
 func (s *openSavesServer) storeRecordInCache(ctx context.Context, key string, record *record.Record) {
-	by, err := cache.EncodeRecord(record)
-	if err != nil {
+	if err := s.cacheStore.Set(ctx, record); err != nil {
 		// Cache fails should be logged but not return error.
 		log.Warnf("failed to encode record for cache for key (%s): %v", key, err)
-	} else {
-		if len(by) < maxRecordSizeToCache {
-			if err := s.cacheStore.Set(ctx, key, by); err != nil {
-				log.Warnf("failed to update cache for key (%s): %v", key, err)
-			}
-		}
-		// TODO(yuryu): should delete the entry anyway in the case
-		// record size grows and gets too big to cache but still needs to be invalidated.
 	}
+	// TODO(yuryu): should delete the entry anyway in the case
+	// record size grows and gets too big to cache but still needs to be invalidated.
 }
 
 // shouldCache returns whether or not Open Saves should try to store
diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go
index aee9d98..b8e07b9 100644
--- a/internal/pkg/cache/cache.go
+++ b/internal/pkg/cache/cache.go
@@ -15,46 +15,75 @@
 package cache
 
 import (
-	"bytes"
 	"context"
-	"encoding/gob"
-	"fmt"
-
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/record"
 )
 
-// Cache interface defines common operations for the cache store.
-type Cache interface {
-	Set(ctx context.Context, key string, value []byte) error
-	Get(ctx context.Context, key string) ([]byte, error)
-	Delete(ctx context.Context, key string) error
-	ListKeys(ctx context.Context) ([]string, error)
-	FlushAll(ctx context.Context) error
+const defaultMaxSizeToCache int = 10 * 1024 * 1024 // 10 MB
+
+// Cache defines operations for a cache service.
+type Cache struct {
+	driver         Driver
+	MaxSizeToCache int
 }
 
-// FormatKey concatenates store and record keys separated by a backslash.
-func FormatKey(storeKey, recordKey string) string {
-	return fmt.Sprintf("%s/%s", storeKey, recordKey)
+func New(driver Driver) *Cache {
+	return &Cache{
+		driver:         driver,
+		MaxSizeToCache: defaultMaxSizeToCache,
+	}
 }
 
-// EncodeRecord serializes a Open Saves Record with gob.
-func EncodeRecord(r *record.Record) ([]byte, error) {
-	b := bytes.Buffer{}
-	e := gob.NewEncoder(&b)
-	if err := e.Encode(r); err != nil {
-		return nil, err
+// Set takes a Cacheable object, encodes it into binary, checks the length, and
+// set the value into the cache if it's under MaxSizeToCache.
+// If the object exceeds MaxSizeToCache, Set deletes the object from the cache.
+func (c *Cache) Set(ctx context.Context, object Cacheable) error {
+	encoded, err := object.EncodeBytes()
+	if err != nil {
+		return err
+	}
+	if len(encoded) > c.MaxSizeToCache {
+		return c.Delete(ctx, object.CacheKey())
 	}
-	return b.Bytes(), nil
+	return c.driver.Set(ctx, object.CacheKey(), encoded)
 }
 
-// DecodeRecord deserializes a Open Saves Record with gob.
-func DecodeRecord(by []byte) (*record.Record, error) {
-	r := &record.Record{}
-	b := bytes.Buffer{}
-	b.Write(by)
-	d := gob.NewDecoder(&b)
-	if err := d.Decode(&r); err != nil {
-		return nil, err
+// Get takes a key string, and a Cacheable object. It fetches an object from the cache,
+// and decodes the binary into dest if it is found. Otherwise it returns a error from
+// Driver.Get.
+func (c *Cache) Get(ctx context.Context, key string, dest Cacheable) error {
+	stored, err := c.driver.Get(ctx, key)
+	if err != nil {
+		return err
 	}
-	return r, nil
+	return dest.DecodeBytes(stored)
+}
+
+// Deletes deletes an object identified by key from the cache.
+func (c *Cache) Delete(ctx context.Context, key string) error {
+	return c.driver.Delete(ctx, key)
+}
+
+// FlushAll clears all entries in the cache.
+func (c *Cache) FlushAll(ctx context.Context) error {
+	return c.driver.FlushAll(ctx)
+}
+
+// Cacheable is an interface that objects implement to support caching.
+type Cacheable interface {
+	// CacheKey returns a cache key string to manage cached entries.
+	CacheKey() string
+	// DecodeBytes deserializes the byte slice given by by.
+	// It can assume ownership and destroy the content of by.
+	DecodeBytes(by []byte) error
+	// EncodeBytes returns a serialized byte slice of the object.
+	EncodeBytes() ([]byte, error)
+}
+
+// Driver interface defines common operations for the cache store.
+type Driver interface {
+	Set(ctx context.Context, key string, value []byte) error
+	Get(ctx context.Context, key string) ([]byte, error)
+	Delete(ctx context.Context, key string) error
+	ListKeys(ctx context.Context) ([]string, error)
+	FlushAll(ctx context.Context) error
 }
diff --git a/internal/pkg/metadb/record/record.go b/internal/pkg/metadb/record/record.go
index 565f13b..1d94452 100644
--- a/internal/pkg/metadb/record/record.go
+++ b/internal/pkg/metadb/record/record.go
@@ -15,9 +15,13 @@
 package record
 
 import (
+	"bytes"
+	"encoding/gob"
+
 	"cloud.google.com/go/datastore"
 	"github.com/google/uuid"
 	pb "github.com/googleforgames/open-saves/api"
+	"github.com/googleforgames/open-saves/internal/pkg/cache"
 	"github.com/googleforgames/open-saves/internal/pkg/metadb/timestamps"
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
@@ -40,12 +44,20 @@ type Record struct {
 	// Timestamps keeps track of creation and modification times and stores a randomly
 	// generated UUID to maintain consistency.
 	Timestamps timestamps.Timestamps
+
+	// StoreKey is used to generate a cache key and needs to be set
+	// before calling the CacheKey function.
+	// It is automatically set when read from Datastore.
+	StoreKey string `datastore:"-"`
 }
 
 // Assert Record implements both PropertyLoadSave and KeyLoader.
 var _ datastore.PropertyLoadSaver = new(Record)
 var _ datastore.KeyLoader = new(Record)
 
+// Assert Record implements Cacheable.
+var _ cache.Cacheable = new(Record)
+
 const externalBlobPropertyName = "ExternalBlob"
 
 // Save and Load for Record replicate the default behaviors, however, they are
@@ -78,9 +90,12 @@ func (r *Record) Load(ps []datastore.Property) error {
 	return datastore.LoadStruct(r, ps)
 }
 
-// LoadKey implements the KeyLoader interface and sets the value to the Key field.
+// LoadKey implements the KeyLoader interface and sets the value to the Key and StoreKey fields.
 func (r *Record) LoadKey(k *datastore.Key) error {
 	r.Key = k.Name
+	if k.Parent != nil {
+		r.StoreKey = k.Parent.Name
+	}
 	return nil
 }
 
@@ -101,7 +116,7 @@ func (r *Record) ToProto() *pb.Record {
 
 // FromProto creates a new Record instance from a proto.
 // Passing nil returns a zero-initialized proto.
-func FromProto(p *pb.Record) *Record {
+func FromProto(storeKey string, p *pb.Record) *Record {
 	if p == nil {
 		return new(Record)
 	}
@@ -116,5 +131,37 @@ func FromProto(p *pb.Record) *Record {
 			CreatedAt: p.GetCreatedAt().AsTime(),
 			UpdatedAt: p.GetUpdatedAt().AsTime(),
 		},
+		StoreKey: storeKey,
+	}
+}
+
+// CacheKey returns a cache key string to manage cached entries.
+// concatenates store and record keys separated by a slash.
+func CacheKey(storeKey, key string) string {
+	return storeKey + "/" + key
+}
+
+// Cacheable implementations.
+
+// CacheKey returns a cache key string to manage cached entries.
+// concatenates store and record keys separated by a slash.
+func (r *Record) CacheKey() string {
+	return CacheKey(r.StoreKey, r.Key)
+}
+
+// DecodeBytes deserializes the byte slice given by by.
+func (r *Record) DecodeBytes(by []byte) error {
+	b := bytes.NewBuffer(by)
+	d := gob.NewDecoder(b)
+	return d.Decode(r)
+}
+
+// EncodeBytes returns a serialized byte slice of the object.
+func (r *Record) EncodeBytes() ([]byte, error) {
+	b := new(bytes.Buffer)
+	e := gob.NewEncoder(b)
+	if err := e.Encode(r); err != nil {
+		return nil, err
 	}
+	return b.Bytes(), nil
 }


Test patch:
diff --git a/internal/app/server/open_saves_test.go b/internal/app/server/open_saves_test.go
index c5cb0f4..861953b 100644
--- a/internal/app/server/open_saves_test.go
+++ b/internal/app/server/open_saves_test.go
@@ -25,7 +25,6 @@ import (
 	"github.com/google/uuid"
 	pb "github.com/googleforgames/open-saves/api"
 	"github.com/googleforgames/open-saves/internal/pkg/blob"
-	"github.com/googleforgames/open-saves/internal/pkg/cache"
 	"github.com/googleforgames/open-saves/internal/pkg/metadb/blobref"
 	"github.com/googleforgames/open-saves/internal/pkg/metadb/record"
 	"github.com/stretchr/testify/assert"
@@ -406,7 +405,7 @@ func TestOpenSaves_CacheRecordsWithHints(t *testing.T) {
 	assert.Equal(t, created.GetCreatedAt(), created.GetUpdatedAt())
 
 	// Check do not cache hint was honored.
-	key := cache.FormatKey(storeKey, recordKey)
+	key := record.CacheKey(storeKey, recordKey)
 	recFromCache, _ := server.getRecordFromCache(ctx, key)
 	assert.Nil(t, recFromCache, "should not have retrieved record from cache after Create with DoNotCache hint")
 
diff --git a/internal/pkg/cache/cache_test.go b/internal/pkg/cache/cache_test.go
index 418c883..8a67f84 100644
--- a/internal/pkg/cache/cache_test.go
+++ b/internal/pkg/cache/cache_test.go
@@ -15,63 +15,84 @@
 package cache
 
 import (
+	"bytes"
+	"context"
 	"testing"
-	"time"
 
-	pb "github.com/googleforgames/open-saves/api"
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/metadbtest"
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/record"
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/timestamps"
+	"github.com/golang/mock/gomock"
+	mock_cache "github.com/googleforgames/open-saves/internal/pkg/cache/mock"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestCache_SerializeRecord(t *testing.T) {
-	rr := []*record.Record{
-		{
-			Timestamps: timestamps.Timestamps{
-				CreatedAt: time.Unix(100, 0),
-				UpdatedAt: time.Unix(110, 0),
-			},
-		},
-		{
-			Key: "some-key",
-			Properties: record.PropertyMap{
-				"prop1": {
-					Type:         pb.Property_BOOLEAN,
-					BooleanValue: false,
-				},
-				"prop2": {
-					Type:         pb.Property_INTEGER,
-					IntegerValue: 200,
-				},
-				"prop3": {
-					Type:        pb.Property_STRING,
-					StringValue: "string value",
-				},
-			},
-			Timestamps: timestamps.Timestamps{
-				CreatedAt: time.Unix(100, 0),
-				UpdatedAt: time.Unix(110, 0),
-			},
-		},
-		{
-			Key:      "some-key",
-			Blob:     []byte("some-bytes"),
-			BlobSize: 64,
-			OwnerID:  "new-owner",
-			Tags:     []string{"tag1", "tag2"},
-			Timestamps: timestamps.Timestamps{
-				CreatedAt: time.Unix(100, 0),
-				UpdatedAt: time.Unix(110, 0),
-			},
-		},
+func TestCache_Simple(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	cacheable := mock_cache.NewMockCacheable(ctrl)
+	driver := mock_cache.NewMockDriver(ctrl)
+	const testCacheKey = "testcache/key"
+	testBinary := []byte{0x42, 0x24, 0x00, 0x12}
+	ctx := context.Background()
+
+	cache := New(driver)
+	if cache == nil {
+		t.Fatal("cache.New returned nil")
+	}
+
+	cacheable.EXPECT().CacheKey().Return(testCacheKey)
+	cacheable.EXPECT().EncodeBytes().Return(testBinary, nil)
+	driver.EXPECT().Set(ctx, testCacheKey, testBinary).Return(nil)
+
+	assert.NoError(t, cache.Set(ctx, cacheable))
+
+	cacheable.EXPECT().DecodeBytes(testBinary).Return(nil)
+	driver.EXPECT().Get(ctx, testCacheKey).Return(testBinary, nil)
+
+	assert.NoError(t, cache.Get(ctx, testCacheKey, cacheable))
+
+	driver.EXPECT().Delete(ctx, testCacheKey).Return(nil)
+	assert.NoError(t, cache.Delete(ctx, testCacheKey))
+
+	driver.EXPECT().FlushAll(ctx).Return(nil)
+	assert.NoError(t, cache.FlushAll(ctx))
+}
+
+func TestCache_TooBigToCache(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	cacheable := mock_cache.NewMockCacheable(ctrl)
+	driver := mock_cache.NewMockDriver(ctrl)
+	const testCacheKey = "testcache/key"
+	ctx := context.Background()
+
+	cache := New(driver)
+	if cache == nil {
+		t.Fatal("cache.New returned nil")
 	}
+	cache.MaxSizeToCache = 100
 
-	for _, r := range rr {
-		e, err := EncodeRecord(r)
-		assert.NoError(t, err)
-		d, err := DecodeRecord(e)
-		assert.NoError(t, err)
-		metadbtest.AssertEqualRecord(t, r, d)
+	testBinaryBuf := new(bytes.Buffer)
+	testBinaryBuf.Grow(cache.MaxSizeToCache + 1)
+	for i := 0; i < cache.MaxSizeToCache; i++ {
+		if err := testBinaryBuf.WriteByte(byte(i)); err != nil {
+			t.Fatalf("test internal error: Buffer.WriteByte returned error: %v", err)
+		}
 	}
+
+	testBytes := testBinaryBuf.Bytes()
+
+	cacheable.EXPECT().EncodeBytes().Return(testBytes, nil)
+	cacheable.EXPECT().CacheKey().Return(testCacheKey)
+	driver.EXPECT().Set(ctx, testCacheKey, testBytes).Return(nil)
+
+	assert.NoError(t, cache.Set(ctx, cacheable))
+
+	// Add one byte
+	if err := testBinaryBuf.WriteByte(0x42); err != nil {
+		t.Fatalf("test internal error: Buffer.WriteByte returned error: %v", err)
+	}
+	testBytes = testBinaryBuf.Bytes()
+
+	cacheable.EXPECT().EncodeBytes().Return(testBytes, nil)
+	cacheable.EXPECT().CacheKey().Return(testCacheKey)
+	driver.EXPECT().Delete(ctx, testCacheKey).Return(nil)
+
+	assert.NoError(t, cache.Set(ctx, cacheable))
 }
diff --git a/internal/pkg/cache/redis/redis_test.go b/internal/pkg/cache/redis/redis_test.go
index 0821721..6c56aed 100644
--- a/internal/pkg/cache/redis/redis_test.go
+++ b/internal/pkg/cache/redis/redis_test.go
@@ -17,13 +17,7 @@ package redis
 import (
 	"context"
 	"testing"
-	"time"
 
-	pb "github.com/googleforgames/open-saves/api"
-	"github.com/googleforgames/open-saves/internal/pkg/cache"
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/metadbtest"
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/record"
-	"github.com/googleforgames/open-saves/internal/pkg/metadb/timestamps"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -63,69 +57,3 @@ func TestRedis_All(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Empty(t, keys)
 }
-
-func TestRedis_SerializeRecord(t *testing.T) {
-	ctx := context.Background()
-
-	red := NewRedis("localhost:6379")
-
-	rr := []*record.Record{
-		{
-			Key: "key1",
-			Timestamps: timestamps.Timestamps{
-				CreatedAt: time.Unix(100, 0),
-				UpdatedAt: time.Unix(110, 0),
-			},
-		},
-		{
-			Key: "key2",
-			Properties: record.PropertyMap{
-				"prop1": {
-					Type:         pb.Property_BOOLEAN,
-					BooleanValue: false,
-				},
-				"prop2": {
-					Type:         pb.Property_INTEGER,
-					IntegerValue: 200,
-				},
-				"prop3": {
-					Type:        pb.Property_STRING,
-					StringValue: "string value",
-				},
-			},
-			Timestamps: timestamps.Timestamps{
-				CreatedAt: time.Unix(100, 0),
-				UpdatedAt: time.Unix(110, 0),
-			},
-		},
-		{
-			Key:      "key3",
-			Blob:     []byte("some-bytes"),
-			BlobSize: 64,
-			OwnerID:  "new-owner",
-			Tags:     []string{"tag1", "tag2"},
-			Timestamps: timestamps.Timestamps{
-				CreatedAt: time.Unix(100, 0),
-				UpdatedAt: time.Unix(110, 0),
-			},
-		},
-	}
-
-	for _, r := range rr {
-		e, err := cache.EncodeRecord(r)
-		assert.NoError(t, err)
-		d, err := cache.DecodeRecord(e)
-		assert.NoError(t, err)
-		metadbtest.AssertEqualRecord(t, r, d)
-
-		red.Set(ctx, r.Key, e)
-		record, err := red.Get(ctx, r.Key)
-		assert.NoError(t, err)
-		assert.Equal(t, e, record)
-		decodedRecord, err := cache.DecodeRecord(record)
-		assert.NoError(t, err)
-		metadbtest.AssertEqualRecord(t, r, decodedRecord)
-
-		assert.NoError(t, red.FlushAll(ctx))
-	}
-}
diff --git a/internal/pkg/metadb/record/record_test.go b/internal/pkg/metadb/record/record_test.go
index d34610e..0bb2c9d 100644
--- a/internal/pkg/metadb/record/record_test.go
+++ b/internal/pkg/metadb/record/record_test.go
@@ -298,14 +298,15 @@ func TestRecord_NewRecordFromProto(t *testing.T) {
 			CreatedAt: createdAt,
 			UpdatedAt: updatedAt,
 		},
+		StoreKey: "test store key",
 	}
-	actual := FromProto(proto)
+	actual := FromProto("test store key", proto)
 	assert.Equal(t, expected, actual)
 }
 
 func TestRecord_NewRecordFromProtoNil(t *testing.T) {
 	expected := new(Record)
-	actual := FromProto(nil)
+	actual := FromProto("", nil)
 	assert.NotNil(t, actual)
 	assert.Equal(t, expected, actual)
 }
@@ -315,6 +316,12 @@ func TestRecord_LoadKey(t *testing.T) {
 	key := datastore.NameKey("kind", "testkey", nil)
 	assert.NoError(t, record.LoadKey(key))
 	assert.Equal(t, "testkey", record.Key)
+	assert.Empty(t, record.StoreKey)
+
+	key = datastore.NameKey("kind", "testkey2", datastore.NameKey("parent", "parentkey", nil))
+	assert.NoError(t, record.LoadKey(key))
+	assert.Equal(t, "testkey2", record.Key)
+	assert.Equal(t, "parentkey", record.StoreKey)
 }
 
 func TestRecord_TestBlobUUID(t *testing.T) {
@@ -335,3 +342,63 @@ func TestRecord_TestBlobUUID(t *testing.T) {
 	assert.NoError(t, err, "Load should not return error")
 	assert.Equal(t, record, actual)
 }
+
+func TestRecord_CacheKey(t *testing.T) {
+	r := &Record{
+		Key:      "abc",
+		StoreKey: "def",
+	}
+	key := r.CacheKey()
+	assert.Equal(t, "def/abc", key)
+}
+
+func TestRecord_SerializeRecord(t *testing.T) {
+	rr := []*Record{
+		{
+			Timestamps: timestamps.Timestamps{
+				CreatedAt: time.Unix(100, 0),
+				UpdatedAt: time.Unix(110, 0),
+			},
+		},
+		{
+			Key: "some-key",
+			Properties: PropertyMap{
+				"prop1": {
+					Type:         pb.Property_BOOLEAN,
+					BooleanValue: false,
+				},
+				"prop2": {
+					Type:         pb.Property_INTEGER,
+					IntegerValue: 200,
+				},
+				"prop3": {
+					Type:        pb.Property_STRING,
+					StringValue: "string value",
+				},
+			},
+			Timestamps: timestamps.Timestamps{
+				CreatedAt: time.Unix(100, 0),
+				UpdatedAt: time.Unix(110, 0),
+			},
+		},
+		{
+			Key:      "some-key",
+			Blob:     []byte("some-bytes"),
+			BlobSize: 64,
+			OwnerID:  "new-owner",
+			Tags:     []string{"tag1", "tag2"},
+			Timestamps: timestamps.Timestamps{
+				CreatedAt: time.Unix(100, 0),
+				UpdatedAt: time.Unix(110, 0),
+			},
+		},
+	}
+
+	for _, r := range rr {
+		e, err := r.EncodeBytes()
+		assert.NoError(t, err)
+		decoded := new(Record)
+		assert.NoError(t, decoded.DecodeBytes(e))
+		assert.Equal(t, r, decoded)
+	}
+}