From 13d5328c7a951f2048d53f916a2f9e9796cd026c Mon Sep 17 00:00:00 2001 From: David Finkel Date: Wed, 24 May 2023 15:54:42 -0400 Subject: [PATCH 1/2] protocodec: add BackendGetterV2 Add a convenience wrapper to make writing BackendGetter implementations that return a protobuf type a bit more convenient. This new type has a constructor to take advantage of the much more powerful function-call-type-parameter inference. --- protocodec/backend_getter_v2.go | 48 ++++++++++++ protocodec/backend_getter_v2_test.go | 106 +++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 protocodec/backend_getter_v2.go create mode 100644 protocodec/backend_getter_v2_test.go diff --git a/protocodec/backend_getter_v2.go b/protocodec/backend_getter_v2.go new file mode 100644 index 00000000..4c309660 --- /dev/null +++ b/protocodec/backend_getter_v2.go @@ -0,0 +1,48 @@ +//go:build go1.18 + +package protocodec + +import ( + "context" + "fmt" + + "github.com/vimeo/galaxycache" + + "google.golang.org/protobuf/proto" +) + +// BackendGetterV2 is an adapter that implements galaxycache.BackendGetter +// (it wraps an unexported type because type-inference is much better on function-calls) +func BackendGetterV2[C any, T pointerMessage[C]](f func(ctx context.Context, key string) (T, error)) galaxycache.BackendGetter { + return backendGetterV2[C, T](f) +} + +// backendGetterV2 is an adapter type that implements galaxycache.BackendGetter +type backendGetterV2[C any, T pointerMessage[C]] func(ctx context.Context, key string) (T, error) + +// Get populates dest with the value identified by key +// The returned data must be unversioned. That is, key must +// uniquely describe the loaded data, without an implicit +// current time, and without relying on cache expiration +// mechanisms. +func (b backendGetterV2[C, T]) Get(ctx context.Context, key string, dest galaxycache.Codec) error { + out, bgErr := b(ctx, key) + if bgErr != nil { + return bgErr + } + switch d := dest.(type) { + case *CodecV2[C, T]: + d.Set(out) + default: + vs, mErr := proto.Marshal(out) + if mErr != nil { + return fmt.Errorf("failed to marshal value as bytes: %w", mErr) + } + + if uErr := dest.UnmarshalBinary(vs); uErr != nil { + return fmt.Errorf("destination codec (type %T) Unmarshal failed: %w", dest, uErr) + } + } + + return nil +} diff --git a/protocodec/backend_getter_v2_test.go b/protocodec/backend_getter_v2_test.go new file mode 100644 index 00000000..1de6b632 --- /dev/null +++ b/protocodec/backend_getter_v2_test.go @@ -0,0 +1,106 @@ +//go:build go1.18 + +package protocodec_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "google.golang.org/protobuf/proto" + + "github.com/vimeo/galaxycache" + "github.com/vimeo/galaxycache/protocodec" + "github.com/vimeo/galaxycache/protocodec/internal/testpbv2" +) + +// Test of common-good-case +func TestBackendGetterV2Good(t *testing.T) { + beGood := func(ctx context.Context, key string) (*testpbv2.TestMessage, error) { + return &testpbv2.TestMessage{ + Name: proto.String("TestName"), + City: proto.String("TestCity"), + }, nil + } + + be := protocodec.BackendGetterV2(beGood) + + ctx := context.Background() + + // test with a proto codec passed (local common-case) + { + pc := protocodec.NewV2[testpbv2.TestMessage]() + + if getErr := be.Get(ctx, "foobar", &pc); getErr != nil { + t.Errorf("noop Get call failed: %s", getErr) + } + + pv := pc.Get() + if pv.City == nil || *pv.City != "TestCity" { + t.Errorf("unexpected value for City: %v", pv.City) + } + if pv.Name == nil || *pv.Name != "TestName" { + t.Errorf("unexpected value for Name: %v", pv.Name) + } + } + // test with a ByteCodec to exercise the common-case when a remote-fetch is done + { + c := galaxycache.ByteCodec{} + + if getErr := be.Get(ctx, "foobar", &c); getErr != nil { + t.Errorf("noop Get call failed: %s", getErr) + } + + if len(c) < len("TestName")+len("TestCity") { + t.Errorf("marshaled bytes too short (less than sum of two string fields)") + } + + pc := protocodec.NewV2[testpbv2.TestMessage]() + + if umErr := pc.UnmarshalBinary([]byte(c)); umErr != nil { + t.Errorf("failed to unmarshal bytes: %s", umErr) + } + + pv := pc.Get() + if pv.City == nil || *pv.City != "TestCity" { + t.Errorf("unexpected value for City: %v", pv.City) + } + if pv.Name == nil || *pv.Name != "TestName" { + t.Errorf("unexpected value for Name: %v", pv.Name) + } + } +} + +func TestBackendGetterV2Bad(t *testing.T) { + sentinel := errors.New("sentinel error") + + beErrorer := func(ctx context.Context, key string) (*testpbv2.TestMessage, error) { + return nil, fmt.Errorf("error: %w", sentinel) + } + + be := protocodec.BackendGetterV2(beErrorer) + + ctx := context.Background() + + // test with a proto codec passed (local common-case) + { + pc := protocodec.NewV2[testpbv2.TestMessage]() + + if getErr := be.Get(ctx, "foobar", &pc); getErr == nil { + t.Errorf("noop Get call didn't fail") + } else if !errors.Is(getErr, sentinel) { + t.Errorf("Error from Get did not wrap/equal sentinel") + } + } + // test with a ByteCodec to exercise the common-case when a remote-fetch is done + { + c := galaxycache.ByteCodec{} + + if getErr := be.Get(ctx, "foobar", &c); getErr == nil { + t.Errorf("noop Get call didn't fail") + } else if !errors.Is(getErr, sentinel) { + t.Errorf("Error from Get did not wrap/equal sentinel") + } + } +} From 108ab0f5e3d3721480f07308c56b3ab7838f94ca Mon Sep 17 00:00:00 2001 From: David Finkel Date: Wed, 24 May 2023 16:07:26 -0400 Subject: [PATCH 2/2] protocodec: add a convenience GalaxyGet method We may add a similar version to the top-level package later, but for now, type-inference leans in the direction of making it codec-specific. --- protocodec/galaxywrap_v2.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 protocodec/galaxywrap_v2.go diff --git a/protocodec/galaxywrap_v2.go b/protocodec/galaxywrap_v2.go new file mode 100644 index 00000000..2c02111c --- /dev/null +++ b/protocodec/galaxywrap_v2.go @@ -0,0 +1,20 @@ +//go:build go1.18 + +package protocodec + +import ( + "context" + + "github.com/vimeo/galaxycache" +) + +// GalaxyGet is a simple wrapper around a Galaxy.Get method-call that takes +// care of constructing the protocodec.CodecV2, etc. (making the interface more idomatic for Go) +func GalaxyGet[C any, T pointerMessage[C]](ctx context.Context, g *galaxycache.Galaxy, key string) (T, error) { + pc := NewV2[C, T]() + getErr := g.Get(ctx, key, &pc) + if getErr != nil { + return nil, getErr + } + return pc.Get(), nil +}