micro: rewrite options to support multiple building blocks

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
2021-01-29 13:17:32 +03:00
parent ac8a3a12c4
commit 827d467077
57 changed files with 1283 additions and 644 deletions

34
register/context.go Normal file
View File

@@ -0,0 +1,34 @@
package register
import (
"context"
)
type registerKey struct{}
// FromContext get register from context
func FromContext(ctx context.Context) (Register, bool) {
if ctx == nil {
return nil, false
}
c, ok := ctx.Value(registerKey{}).(Register)
return c, ok
}
// NewContext put register in context
func NewContext(ctx context.Context, c Register) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, registerKey{}, c)
}
// SetOption returns a function to setup a context with given value
func SetOption(k, v interface{}) Option {
return func(o *Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

140
register/extractor.go Normal file
View File

@@ -0,0 +1,140 @@
package register
import (
"fmt"
"reflect"
"strings"
"unicode"
"unicode/utf8"
"github.com/unistack-org/micro/v3/metadata"
)
// Extract *Value from reflect.Type
func ExtractValue(v reflect.Type, d int) *Value {
if d == 3 {
return nil
}
if v == nil {
return nil
}
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if len(v.Name()) == 0 {
return nil
}
// get the rune character
a, _ := utf8.DecodeRuneInString(string(v.Name()[0]))
// crude check for is unexported field
if unicode.IsLower(a) {
return nil
}
arg := &Value{
Name: v.Name(),
Type: v.Name(),
}
switch v.Kind() {
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
val := ExtractValue(f.Type, d+1)
if val == nil {
continue
}
// if we can find a json tag use it
if tags := f.Tag.Get("json"); len(tags) > 0 {
parts := strings.Split(tags, ",")
if parts[0] == "-" || parts[0] == "omitempty" {
continue
}
val.Name = parts[0]
}
// if there's no name default it
if len(val.Name) == 0 {
val.Name = v.Field(i).Name
}
arg.Values = append(arg.Values, val)
}
case reflect.Slice:
p := v.Elem()
if p.Kind() == reflect.Ptr {
p = p.Elem()
}
arg.Type = "[]" + p.Name()
}
return arg
}
// ExtractEndpoint extract *Endpoint from reflect.Method
func ExtractEndpoint(method reflect.Method) *Endpoint {
if method.PkgPath != "" {
return nil
}
var rspType, reqType reflect.Type
var stream bool
mt := method.Type
switch mt.NumIn() {
case 3:
reqType = mt.In(1)
rspType = mt.In(2)
case 4:
reqType = mt.In(2)
rspType = mt.In(3)
default:
return nil
}
// are we dealing with a stream?
switch rspType.Kind() {
case reflect.Func, reflect.Interface:
stream = true
}
request := ExtractValue(reqType, 0)
response := ExtractValue(rspType, 0)
if request == nil || response == nil {
return nil
}
ep := &Endpoint{
Name: method.Name,
Request: request,
Response: response,
Metadata: metadata.New(0),
}
if stream {
ep.Metadata.Set("stream", fmt.Sprintf("%v", stream))
}
return ep
}
// ExtractSubValue exctact *Value from reflect.Type
func ExtractSubValue(typ reflect.Type) *Value {
var reqType reflect.Type
switch typ.NumIn() {
case 1:
reqType = typ.In(0)
case 2:
reqType = typ.In(1)
case 3:
reqType = typ.In(2)
default:
return nil
}
return ExtractValue(reqType, 0)
}

View File

@@ -0,0 +1,63 @@
package register
import (
"context"
"reflect"
"testing"
)
type TestHandler struct{}
type TestRequest struct{}
type TestResponse struct{}
func (t *TestHandler) Test(ctx context.Context, req *TestRequest, rsp *TestResponse) error {
return nil
}
func TestExtractEndpoint(t *testing.T) {
handler := &TestHandler{}
typ := reflect.TypeOf(handler)
var endpoints []*Endpoint
for m := 0; m < typ.NumMethod(); m++ {
if e := ExtractEndpoint(typ.Method(m)); e != nil {
endpoints = append(endpoints, e)
}
}
if i := len(endpoints); i != 1 {
t.Fatalf("Expected 1 endpoint, have %d", i)
}
if endpoints[0].Name != "Test" {
t.Fatalf("Expected handler Test, got %s", endpoints[0].Name)
}
if endpoints[0].Request == nil {
t.Fatal("Expected non nil Request")
}
if endpoints[0].Response == nil {
t.Fatal("Expected non nil Request")
}
if endpoints[0].Request.Name != "TestRequest" {
t.Fatalf("Expected TestRequest got %s", endpoints[0].Request.Name)
}
if endpoints[0].Response.Name != "TestResponse" {
t.Fatalf("Expected TestResponse got %s", endpoints[0].Response.Name)
}
if endpoints[0].Request.Type != "TestRequest" {
t.Fatalf("Expected TestRequest type got %s", endpoints[0].Request.Type)
}
if endpoints[0].Response.Type != "TestResponse" {
t.Fatalf("Expected TestResponse type got %s", endpoints[0].Response.Type)
}
}

85
register/noop.go Normal file
View File

@@ -0,0 +1,85 @@
package register
import (
"context"
)
type noopRegister struct {
opts Options
}
func (n *noopRegister) Name() string {
return n.opts.Name
}
// Init initialize register
func (n *noopRegister) Init(opts ...Option) error {
for _, o := range opts {
o(&n.opts)
}
return nil
}
// Options returns options struct
func (n *noopRegister) Options() Options {
return n.opts
}
// Connect opens connection to register
func (n *noopRegister) Connect(ctx context.Context) error {
return nil
}
// Disconnect close connection to register
func (n *noopRegister) Disconnect(ctx context.Context) error {
return nil
}
// Register registers service
func (n *noopRegister) Register(ctx context.Context, svc *Service, opts ...RegisterOption) error {
return nil
}
// Deregister deregisters service
func (n *noopRegister) Deregister(ctx context.Context, svc *Service, opts ...DeregisterOption) error {
return nil
}
// LookupService returns servive info
func (n *noopRegister) LookupService(ctx context.Context, name string, opts ...LookupOption) ([]*Service, error) {
return []*Service{}, nil
}
// ListServices listing services
func (n *noopRegister) ListServices(ctx context.Context, opts ...ListOption) ([]*Service, error) {
return []*Service{}, nil
}
// Watch is used to watch for service changes
func (n *noopRegister) Watch(ctx context.Context, opts ...WatchOption) (Watcher, error) {
return &noopWatcher{done: make(chan struct{}), opts: NewWatchOptions(opts...)}, nil
}
// String returns register string representation
func (n *noopRegister) String() string {
return "noop"
}
type noopWatcher struct {
opts WatchOptions
done chan struct{}
}
func (n *noopWatcher) Next() (*Result, error) {
<-n.done
return nil, ErrWatcherStopped
}
func (n *noopWatcher) Stop() {
close(n.done)
}
// NewRegister returns a new noop register
func NewRegister(opts ...Option) Register {
return &noopRegister{opts: NewOptions(opts...)}
}

271
register/options.go Normal file
View File

@@ -0,0 +1,271 @@
package register
import (
"context"
"crypto/tls"
"time"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/meter"
"github.com/unistack-org/micro/v3/tracer"
)
type Options struct {
Name string
Addrs []string
Timeout time.Duration
TLSConfig *tls.Config
// Logger that will be used
Logger logger.Logger
// Meter that will be used
Meter meter.Meter
// Tracer
Tracer tracer.Tracer
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
}
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
Tracer: tracer.DefaultTracer,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
type RegisterOptions struct {
TTL time.Duration
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
// Domain to register the service in
Domain string
// Attempts specify attempts for register
Attempts int
}
func NewRegisterOptions(opts ...RegisterOption) RegisterOptions {
options := RegisterOptions{
Domain: DefaultDomain,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
type WatchOptions struct {
// Specify a service to watch
// If blank, the watch is for all services
Service string
// Other options for implementations of the interface
// can be stored in a context
Context context.Context
// Domain to watch
Domain string
}
func NewWatchOptions(opts ...WatchOption) WatchOptions {
options := WatchOptions{
Domain: DefaultDomain,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
type DeregisterOptions struct {
Context context.Context
// Domain the service was registered in
Domain string
// Atempts specify max attempts for deregister
Attempts int
}
func NewDeregisterOptions(opts ...DeregisterOption) DeregisterOptions {
options := DeregisterOptions{
Domain: DefaultDomain,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
type LookupOptions struct {
Context context.Context
// Domain to scope the request to
Domain string
}
func NewLookupOptions(opts ...LookupOption) LookupOptions {
options := LookupOptions{
Domain: DefaultDomain,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
type ListOptions struct {
Context context.Context
// Domain to scope the request to
Domain string
}
func NewListOptions(opts ...ListOption) ListOptions {
options := ListOptions{
Domain: DefaultDomain,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// Addrs is the register addresses to use
func Addrs(addrs ...string) Option {
return func(o *Options) {
o.Addrs = addrs
}
}
func Timeout(t time.Duration) Option {
return func(o *Options) {
o.Timeout = t
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Tracer sets the tracer
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// Context sets the context
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// Specify TLS Config
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
func RegisterAttempts(t int) RegisterOption {
return func(o *RegisterOptions) {
o.Attempts = t
}
}
func RegisterTTL(t time.Duration) RegisterOption {
return func(o *RegisterOptions) {
o.TTL = t
}
}
func RegisterContext(ctx context.Context) RegisterOption {
return func(o *RegisterOptions) {
o.Context = ctx
}
}
func RegisterDomain(d string) RegisterOption {
return func(o *RegisterOptions) {
o.Domain = d
}
}
// Watch a service
func WatchService(name string) WatchOption {
return func(o *WatchOptions) {
o.Service = name
}
}
func WatchContext(ctx context.Context) WatchOption {
return func(o *WatchOptions) {
o.Context = ctx
}
}
func WatchDomain(d string) WatchOption {
return func(o *WatchOptions) {
o.Domain = d
}
}
func DeregisterTimeout(t int) DeregisterOption {
return func(o *DeregisterOptions) {
o.Attempts = t
}
}
func DeregisterContext(ctx context.Context) DeregisterOption {
return func(o *DeregisterOptions) {
o.Context = ctx
}
}
func DeregisterDomain(d string) DeregisterOption {
return func(o *DeregisterOptions) {
o.Domain = d
}
}
func GetContext(ctx context.Context) LookupOption {
return func(o *LookupOptions) {
o.Context = ctx
}
}
func GetDomain(d string) LookupOption {
return func(o *LookupOptions) {
o.Domain = d
}
}
func ListContext(ctx context.Context) ListOption {
return func(o *ListOptions) {
o.Context = ctx
}
}
func ListDomain(d string) ListOption {
return func(o *ListOptions) {
o.Domain = d
}
}

91
register/register.go Normal file
View File

@@ -0,0 +1,91 @@
// Package register is an interface for service discovery
package register
import (
"context"
"errors"
"github.com/unistack-org/micro/v3/metadata"
)
const (
// WildcardDomain indicates any domain
WildcardDomain = "*"
// DefaultDomain to use if none was provided in options
DefaultDomain = "micro"
)
var (
// DefaultRegister is the global default register
DefaultRegister Register = NewRegister()
// ErrNotFound returned when LookupService is called and no services found
ErrNotFound = errors.New("service not found")
// ErrWatcherStopped returned when when watcher is stopped
ErrWatcherStopped = errors.New("watcher stopped")
)
// Register provides an interface for service discovery
// and an abstraction over varying implementations
// {consul, etcd, zookeeper, ...}
type Register interface {
Name() string
Init(...Option) error
Options() Options
Connect(context.Context) error
Disconnect(context.Context) error
Register(context.Context, *Service, ...RegisterOption) error
Deregister(context.Context, *Service, ...DeregisterOption) error
LookupService(context.Context, string, ...LookupOption) ([]*Service, error)
ListServices(context.Context, ...ListOption) ([]*Service, error)
Watch(context.Context, ...WatchOption) (Watcher, error)
String() string
}
// Service holds service register info
type Service struct {
Name string `json:"name"`
Version string `json:"version"`
Metadata metadata.Metadata `json:"metadata"`
Endpoints []*Endpoint `json:"endpoints"`
Nodes []*Node `json:"nodes"`
}
// Node holds node register info
type Node struct {
Id string `json:"id"`
Address string `json:"address"`
Metadata metadata.Metadata `json:"metadata"`
}
// Endpoint holds endpoint register info
type Endpoint struct {
Name string `json:"name"`
Request *Value `json:"request"`
Response *Value `json:"response"`
Metadata metadata.Metadata `json:"metadata"`
}
// Value holds additional kv stuff
type Value struct {
Name string `json:"name"`
Type string `json:"type"`
Values []*Value `json:"values"`
}
// Option func signature
type Option func(*Options)
// RegisterOption option is used to register service
type RegisterOption func(*RegisterOptions)
// WatchOption option is used to watch service changes
type WatchOption func(*WatchOptions)
// DeregisterOption option is used to deregister service
type DeregisterOption func(*DeregisterOptions)
// LookupOption option is used to get service
type LookupOption func(*LookupOptions)
// ListOption option is used to list services
type ListOption func(*ListOptions)

56
register/watcher.go Normal file
View File

@@ -0,0 +1,56 @@
package register
import "time"
// Watcher is an interface that returns updates
// about services within the register.
type Watcher interface {
// Next is a blocking call
Next() (*Result, error)
Stop()
}
// Result is returned by a call to Next on
// the watcher. Actions can be create, update, delete
type Result struct {
Action string
Service *Service
}
// EventType defines register event type
type EventType int
const (
// Create is emitted when a new service is registered
Create EventType = iota
// Delete is emitted when an existing service is deregistered
Delete
// Update is emitted when an existing service is updated
Update
)
// String returns human readable event type
func (t EventType) String() string {
switch t {
case Create:
return "create"
case Delete:
return "delete"
case Update:
return "update"
default:
return "unknown"
}
}
// Event is register event
type Event struct {
// Id is register id
Id string
// Type defines type of event
Type EventType
// Timestamp is event timestamp
Timestamp time.Time
// Service is register service
Service *Service
}