Skip to content

Commit 8ce67fb

Browse files
authored
feat: add refresh strategy to stencil client (raystack#99)
Features: * add logger interface to `stencil.Options`. Now clients can pass their own logger to stencil client. * Introduced version based refresh strategy BREAKING CHANGE: * Removed NewMultiURLClient method. * Changed NewClient method to accept list of urls, instead of just taking one * Removed ParseWithRefresh, SerializeWithRefresh methods from Client interface * Changed Refresh method signature from `Refresh() error` to `Refresh()`
1 parent a054d68 commit 8ce67fb

12 files changed

+390
-403
lines changed

clients/go/README.md

+32-16
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,16 @@ import stencil "github.com/odpf/stencil/clients/go"
3535
```go
3636
import stencil "github.com/odpf/stencil/clients/go"
3737

38-
url := "http://url/to/proto/descriptorset/file"
39-
client, err := stencil.NewClient(url, stencil.Options{})
40-
```
41-
42-
### Creating a multiURLClient
43-
44-
```go
45-
import stencil "github.com/odpf/stencil/clients/go"
46-
47-
urls := []string{"http://urlA", "http://urlB"}
48-
client, err := stencil.NewMultiURLClient(urls, stencil.Options{})
38+
url := "http://localhost:8000/v1beta1/namespaces/{test-namespace}/schemas/{schema-name}"
39+
client, err := stencil.NewClient([]string{url}, stencil.Options{})
4940
```
5041

5142
### Get Descriptor
5243
```go
5344
import stencil "github.com/odpf/stencil/clients/go"
5445

55-
url := "http://url/to/proto/descriptorset/file"
56-
client, err := stencil.NewClient(url, stencil.Options{})
46+
url := "http://localhost:8000/v1beta1/namespaces/{test-namespace}/schemas/{schema-name}"
47+
client, err := stencil.NewClient([]string{url}, stencil.Options{})
5748
if err != nil {
5849
return
5950
}
@@ -64,8 +55,8 @@ desc, err := client.GetDescriptor("google.protobuf.DescriptorProto")
6455
```go
6556
import stencil "github.com/odpf/stencil/clients/go"
6657

67-
url := "http://url/to/proto/descriptorset/file"
68-
client, err := stencil.NewClient(url, stencil.Options{})
58+
url := "http://localhost:8000/v1beta1/namespaces/{test-namespace}/schemas/{schema-name}"
59+
client, err := stencil.NewClient([]string{url}, stencil.Options{})
6960
if err != nil {
7061
return
7162
}
@@ -78,12 +69,37 @@ parsedMsg, err := client.Parse("google.protobuf.DescriptorProto", data)
7869
import stencil "github.com/odpf/stencil/clients/go"
7970

8071
url := "http://url/to/proto/descriptorset/file"
81-
client, err := stencil.NewClient(url, stencil.Options{})
72+
client, err := stencil.NewClient([]string{url}, stencil.Options{})
8273
if err != nil {
8374
return
8475
}
8576
data := map[string]interface{}{}
8677
serializedMsg, err := client.Serialize("google.protobuf.DescriptorProto", data)
8778
```
8879

80+
### Enable auto refresh of schemas
81+
```go
82+
import stencil "github.com/odpf/stencil/clients/go"
83+
84+
url := "http://localhost:8000/v1beta1/namespaces/{test-namespace}/schemas/{schema-name}"
85+
// Configured to refresh schema every 12 hours
86+
client, err := stencil.NewClient([]string{url}, stencil.Options{AutoRefresh: true, RefreshInterval: time.Hours * 12})
87+
if err != nil {
88+
return
89+
}
90+
desc, err := client.GetDescriptor("google.protobuf.DescriptorProto")
91+
```
92+
93+
### Using VersionBasedRefresh strategy
94+
```go
95+
import stencil "github.com/odpf/stencil/clients/go"
96+
97+
url := "http://localhost:8000/v1beta1/namespaces/{test-namespace}/schemas/{schema-name}"
98+
// Configured to refresh schema every 12 hours
99+
client, err := stencil.NewClient([]string{url}, stencil.Options{AutoRefresh: true, RefreshInterval: time.Hours * 12, RefreshStrategy: stencil.VersionBasedRefresh})
100+
if err != nil {
101+
return
102+
}
103+
desc, err := client.GetDescriptor("google.protobuf.DescriptorProto")
104+
```
89105
Refer to [go documentation](https://pkg.go.dev/github.com/odpf/stencil/clients/go) for all available methods and options.

clients/go/client.go

+57-80
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,19 @@ package stencil
55

66
import (
77
"encoding/json"
8-
"io"
98
"time"
109

10+
"github.com/goburrow/cache"
1111
"github.com/pkg/errors"
12-
"go.uber.org/multierr"
1312
"google.golang.org/protobuf/encoding/protojson"
1413
"google.golang.org/protobuf/proto"
1514
"google.golang.org/protobuf/reflect/protoreflect"
16-
"google.golang.org/protobuf/types/dynamicpb"
1715
)
1816

1917
var (
2018
//ErrNotFound default sentinel error if proto not found
2119
ErrNotFound = errors.New("not found")
22-
//ErrNotFound is for when descriptor does not match the message
20+
//ErrInvalidDescriptor is for when descriptor does not match the message
2321
ErrInvalidDescriptor = errors.New("invalid descriptor")
2422
)
2523

@@ -29,23 +27,17 @@ type Client interface {
2927
// Parse parses protobuf message from wire format to protoreflect.ProtoMessage given fully qualified name of proto message.
3028
// Returns ErrNotFound error if given class name is not found
3129
Parse(string, []byte) (protoreflect.ProtoMessage, error)
32-
// ParseWithRefresh parses protobuf message from wire format to `protoreflect.ProtoMessage` given fully qualified name of proto message.
33-
// Refreshes proto definitions if parsed message has unknown fields and parses the message again.
34-
// Returns ErrNotFound error if given class name is not found.
35-
ParseWithRefresh(string, []byte) (protoreflect.ProtoMessage, error)
3630
// Serialize serializes data to bytes given fully qualified name of proto message.
3731
// Returns ErrNotFound error if given class name is not found
3832
Serialize(string, interface{}) ([]byte, error)
39-
// SerializeWithRefresh serializes data to bytes given fully qualified name of proto message.
40-
// Refreshes proto definitions if message has unknown fields and serialized the message again.
41-
// Returns ErrNotFound error if given class name is not found.
42-
SerializeWithRefresh(string, interface{}) ([]byte, error)
4333
// GetDescriptor returns protoreflect.MessageDescriptor given fully qualified proto java class name
4434
GetDescriptor(string) (protoreflect.MessageDescriptor, error)
4535
// Close stops background refresh if configured.
4636
Close()
47-
// Refresh downloads latest proto definitions
48-
Refresh() error
37+
// Refresh loads new values from specified url. If the schema is already fetched, the previous value
38+
// will continue to be used by Parse methods while the new value is loading.
39+
// If schemas not loaded, then this function will block until the value is loaded.
40+
Refresh()
4941
}
5042

5143
// HTTPOptions options for http client
@@ -66,6 +58,11 @@ type Options struct {
6658
RefreshInterval time.Duration
6759
// HTTPOptions options for http client
6860
HTTPOptions
61+
// RefreshStrategy refresh strategy to use while fetching schema.
62+
// Default strategy set to `stencil.LongPollingRefresh` strategy
63+
RefreshStrategy
64+
// Logger is the interface used to get logging from stencil internals.
65+
Logger
6966
}
7067

7168
func (o *Options) setDefaults() {
@@ -77,50 +74,56 @@ func (o *Options) setDefaults() {
7774
}
7875
}
7976

77+
// NewClient creates stencil client. Downloads proto descriptor file from given url and stores the definitions.
78+
// It will throw error if download fails or downloaded file is not fully contained descriptor file
79+
func NewClient(urls []string, options Options) (Client, error) {
80+
cacheOptions := []cache.Option{cache.WithMaximumSize(len(urls))}
81+
options.setDefaults()
82+
if options.AutoRefresh {
83+
cacheOptions = append(cacheOptions, cache.WithRefreshAfterWrite(options.RefreshInterval), cache.WithExpireAfterWrite(options.RefreshInterval))
84+
}
85+
newCache := cache.NewLoadingCache(options.RefreshStrategy.getLoader(options), cacheOptions...)
86+
s, err := newStore(urls, newCache)
87+
if err != nil {
88+
return nil, err
89+
}
90+
return &stencilClient{urls: urls, store: s, options: options}, nil
91+
}
92+
8093
type stencilClient struct {
81-
timer io.Closer
8294
urls []string
83-
store *descriptorStore
95+
store *store
8496
options Options
8597
}
8698

8799
func (s *stencilClient) Parse(className string, data []byte) (protoreflect.ProtoMessage, error) {
88-
desc, ok := s.store.get(className)
100+
resolver, ok := s.getMatchingResolver(className)
89101
if !ok {
90102
return nil, ErrNotFound
91103
}
92-
m := dynamicpb.NewMessage(desc).New().Interface()
93-
err := proto.UnmarshalOptions{Resolver: s.store.extensionResolver}.Unmarshal(data, m)
104+
messageType, _ := resolver.Get(className)
105+
m := messageType.New().Interface()
106+
err := proto.UnmarshalOptions{Resolver: resolver.GetTypeResolver()}.Unmarshal(data, m)
94107
return m, err
95108
}
96109

97-
func (s *stencilClient) ParseWithRefresh(className string, data []byte) (protoreflect.ProtoMessage, error) {
98-
m, err := s.Parse(className, data)
99-
if err != nil || m.ProtoReflect().GetUnknown() == nil {
100-
return m, err
101-
}
102-
if err = s.Refresh(); err != nil {
103-
return m, err
104-
}
105-
return s.Parse(className, data)
106-
}
107-
108110
func (s *stencilClient) Serialize(className string, data interface{}) (bytes []byte, err error) {
109111
// message to json
110112
jsonBytes, err := json.Marshal(data)
111113
if err != nil {
112114
return
113115
}
114116

115-
// get descriptor
116-
desc, err := s.GetDescriptor(className)
117-
if err != nil {
118-
return
117+
resolver, ok := s.getMatchingResolver(className)
118+
if !ok {
119+
return nil, ErrNotFound
119120
}
120121

122+
// get descriptor
123+
messageType, _ := resolver.Get(className)
121124
// construct proto message
122-
m := dynamicpb.NewMessage(desc).New().Interface()
123-
err = protojson.UnmarshalOptions{Resolver: s.store.extensionResolver}.Unmarshal(jsonBytes, m)
125+
m := messageType.New().Interface()
126+
err = protojson.UnmarshalOptions{Resolver: resolver.GetTypeResolver()}.Unmarshal(jsonBytes, m)
124127
if err != nil {
125128
return bytes, ErrInvalidDescriptor
126129
}
@@ -129,63 +132,37 @@ func (s *stencilClient) Serialize(className string, data interface{}) (bytes []b
129132
return proto.Marshal(m)
130133
}
131134

132-
func (s *stencilClient) SerializeWithRefresh(className string, data interface{}) (bytes []byte, err error) {
133-
bytes, err = s.Serialize(className, data)
134-
if err == nil || (err != ErrNotFound && err != ErrInvalidDescriptor) {
135-
return
136-
}
137-
138-
if err = s.Refresh(); err != nil {
139-
return bytes, errors.Wrap(err, "error refreshing descriptor")
135+
func (s *stencilClient) getMatchingResolver(className string) (*Resolver, bool) {
136+
for _, url := range s.urls {
137+
resolver, ok := s.store.getResolver(url)
138+
if !ok {
139+
return nil, false
140+
}
141+
_, ok = resolver.Get(className)
142+
if ok {
143+
return resolver, ok
144+
}
140145
}
141-
142-
return s.Serialize(className, data)
146+
return nil, false
143147
}
144148

145149
func (s *stencilClient) GetDescriptor(className string) (protoreflect.MessageDescriptor, error) {
146-
desc, ok := s.store.get(className)
150+
resolver, ok := s.getMatchingResolver(className)
147151
if !ok {
148152
return nil, ErrNotFound
149153
}
150-
return desc, nil
154+
desc, _ := resolver.Get(className)
155+
return desc.Descriptor(), nil
151156
}
152157

153158
func (s *stencilClient) Close() {
154-
if s.timer != nil {
155-
s.timer.Close()
159+
if s.store != nil {
160+
s.store.Close()
156161
}
157162
}
158163

159-
func (s *stencilClient) Refresh() error {
160-
var err error
164+
func (s *stencilClient) Refresh() {
161165
for _, url := range s.urls {
162-
err = multierr.Combine(err, s.store.loadFromURI(url, s.options))
166+
s.store.Refresh(url)
163167
}
164-
return err
165-
}
166-
167-
func (s *stencilClient) load() error {
168-
s.options.setDefaults()
169-
if s.options.AutoRefresh {
170-
s.timer = setInterval(s.options.RefreshInterval, func() { s.Refresh() })
171-
}
172-
err := s.Refresh()
173-
return err
174-
}
175-
176-
// NewClient creates stencil client. Downloads proto descriptor file from given url and stores the definitions.
177-
// It will throw error if download fails or downloaded file is not fully contained descriptor file
178-
func NewClient(url string, options Options) (Client, error) {
179-
s := &stencilClient{store: newStore(), urls: []string{url}, options: options}
180-
err := s.load()
181-
return s, err
182-
}
183-
184-
// NewMultiURLClient creates stencil client with multiple urls. Downloads proto descriptor file from given urls and stores the definitions.
185-
// If descriptor files from multiple urls has different schema definitions with same name, last downloaded proto descriptor will override previous entries.
186-
// It will throw error if any of the download fails or any downloaded file is not fully contained descriptor file
187-
func NewMultiURLClient(urls []string, options Options) (Client, error) {
188-
s := &stencilClient{store: newStore(), urls: urls, options: options}
189-
err := s.load()
190-
return s, err
191168
}

0 commit comments

Comments
 (0)