Initial QSfera import
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/util/backoff"
|
||||
)
|
||||
|
||||
type BackoffFunc func(ctx context.Context, req Request, attempts int) (time.Duration, error)
|
||||
|
||||
func exponentialBackoff(ctx context.Context, req Request, attempts int) (time.Duration, error) {
|
||||
return backoff.Do(attempts), nil
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"time"
|
||||
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
)
|
||||
|
||||
// NewCache returns an initialized cache.
|
||||
func NewCache() *Cache {
|
||||
return &Cache{
|
||||
cache: cache.New(cache.NoExpiration, 30*time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for responses.
|
||||
type Cache struct {
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
// Get a response from the cache.
|
||||
func (c *Cache) Get(ctx context.Context, req *Request) (interface{}, bool) {
|
||||
return c.cache.Get(key(ctx, req))
|
||||
}
|
||||
|
||||
// Set a response in the cache.
|
||||
func (c *Cache) Set(ctx context.Context, req *Request, rsp interface{}, expiry time.Duration) {
|
||||
c.cache.Set(key(ctx, req), rsp, expiry)
|
||||
}
|
||||
|
||||
// List the key value pairs in the cache.
|
||||
func (c *Cache) List() map[string]string {
|
||||
items := c.cache.Items()
|
||||
|
||||
rsp := make(map[string]string, len(items))
|
||||
|
||||
for k, v := range items {
|
||||
bytes, _ := json.Marshal(v.Object)
|
||||
rsp[k] = string(bytes)
|
||||
}
|
||||
|
||||
return rsp
|
||||
}
|
||||
|
||||
// key returns a hash for the context and request.
|
||||
func key(ctx context.Context, req *Request) string {
|
||||
ns, _ := metadata.Get(ctx, headers.Namespace)
|
||||
|
||||
bytes, _ := json.Marshal(map[string]interface{}{
|
||||
"namespace": ns,
|
||||
"request": map[string]interface{}{
|
||||
"service": (*req).Service(),
|
||||
"endpoint": (*req).Endpoint(),
|
||||
"method": (*req).Method(),
|
||||
"body": (*req).Body(),
|
||||
},
|
||||
})
|
||||
|
||||
h := fnv.New64()
|
||||
h.Write(bytes)
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Package client is an interface for an RPC client
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
var (
|
||||
// NewClient returns a new client.
|
||||
NewClient func(...Option) Client = newRPCClient
|
||||
// DefaultClient is a default client to use out of the box.
|
||||
DefaultClient Client = newRPCClient()
|
||||
)
|
||||
|
||||
// Client is the interface used to make requests to services.
|
||||
// It supports Request/Response via Transport and Publishing via the Broker.
|
||||
// It also supports bidirectional streaming of requests.
|
||||
type Client interface {
|
||||
Init(...Option) error
|
||||
Options() Options
|
||||
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
|
||||
NewRequest(service, endpoint string, req interface{}, reqOpts ...RequestOption) Request
|
||||
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
||||
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
||||
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// Router manages request routing.
|
||||
type Router interface {
|
||||
SendRequest(context.Context, Request) (Response, error)
|
||||
}
|
||||
|
||||
// Message is the interface for publishing asynchronously.
|
||||
type Message interface {
|
||||
Topic() string
|
||||
Payload() interface{}
|
||||
ContentType() string
|
||||
}
|
||||
|
||||
// Request is the interface for a synchronous request used by Call or Stream.
|
||||
type Request interface {
|
||||
// The service to call
|
||||
Service() string
|
||||
// The action to take
|
||||
Method() string
|
||||
// The endpoint to invoke
|
||||
Endpoint() string
|
||||
// The content type
|
||||
ContentType() string
|
||||
// The unencoded request body
|
||||
Body() interface{}
|
||||
// Write to the encoded request writer. This is nil before a call is made
|
||||
Codec() codec.Writer
|
||||
// indicates whether the request will be a streaming one rather than unary
|
||||
Stream() bool
|
||||
}
|
||||
|
||||
// Response is the response received from a service.
|
||||
type Response interface {
|
||||
// Read the response
|
||||
Codec() codec.Reader
|
||||
// read the header
|
||||
Header() map[string]string
|
||||
// Read the undecoded response
|
||||
Read() ([]byte, error)
|
||||
}
|
||||
|
||||
// Stream is the inteface for a bidirectional synchronous stream.
|
||||
type Stream interface {
|
||||
Closer
|
||||
// Context for the stream
|
||||
Context() context.Context
|
||||
// The request made
|
||||
Request() Request
|
||||
// The response read
|
||||
Response() Response
|
||||
// Send will encode and send a request
|
||||
Send(interface{}) error
|
||||
// Recv will decode and read a response
|
||||
Recv(interface{}) error
|
||||
// Error returns the stream error
|
||||
Error() error
|
||||
// Close closes the stream
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Closer handle client close.
|
||||
type Closer interface {
|
||||
// CloseSend closes the send direction of the stream.
|
||||
CloseSend() error
|
||||
}
|
||||
|
||||
// Option used by the Client.
|
||||
type Option func(*Options)
|
||||
|
||||
// CallOption used by Call or Stream.
|
||||
type CallOption func(*CallOptions)
|
||||
|
||||
// PublishOption used by Publish.
|
||||
type PublishOption func(*PublishOptions)
|
||||
|
||||
// MessageOption used by NewMessage.
|
||||
type MessageOption func(*MessageOptions)
|
||||
|
||||
// RequestOption used by NewRequest.
|
||||
type RequestOption func(*RequestOptions)
|
||||
|
||||
// Makes a synchronous call to a service using the default client.
|
||||
func Call(ctx context.Context, request Request, response interface{}, opts ...CallOption) error {
|
||||
return DefaultClient.Call(ctx, request, response, opts...)
|
||||
}
|
||||
|
||||
// Publishes a publication using the default client. Using the underlying broker
|
||||
// set within the options.
|
||||
func Publish(ctx context.Context, msg Message, opts ...PublishOption) error {
|
||||
return DefaultClient.Publish(ctx, msg, opts...)
|
||||
}
|
||||
|
||||
// Creates a new message using the default client.
|
||||
func NewMessage(topic string, payload interface{}, opts ...MessageOption) Message {
|
||||
return DefaultClient.NewMessage(topic, payload, opts...)
|
||||
}
|
||||
|
||||
// Creates a new request using the default client. Content Type will
|
||||
// be set to the default within options and use the appropriate codec.
|
||||
func NewRequest(service, endpoint string, request interface{}, reqOpts ...RequestOption) Request {
|
||||
return DefaultClient.NewRequest(service, endpoint, request, reqOpts...)
|
||||
}
|
||||
|
||||
// Creates a streaming connection with a service and returns responses on the
|
||||
// channel passed in. It's up to the user to close the streamer.
|
||||
func NewStream(ctx context.Context, request Request, opts ...CallOption) (Stream, error) {
|
||||
return DefaultClient.Stream(ctx, request, opts...)
|
||||
}
|
||||
|
||||
func String() string {
|
||||
return DefaultClient.String()
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type clientKey struct{}
|
||||
|
||||
func FromContext(ctx context.Context) (Client, bool) {
|
||||
c, ok := ctx.Value(clientKey{}).(Client)
|
||||
return c, ok
|
||||
}
|
||||
|
||||
func NewContext(ctx context.Context, c Client) context.Context {
|
||||
return context.WithValue(ctx, clientKey{}, c)
|
||||
}
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
"go-micro.dev/v4/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultBackoff is the default backoff function for retries.
|
||||
DefaultBackoff = exponentialBackoff
|
||||
// DefaultRetry is the default check-for-retry function for retries.
|
||||
DefaultRetry = RetryOnError
|
||||
// DefaultRetries is the default number of times a request is tried.
|
||||
DefaultRetries = 5
|
||||
// DefaultRequestTimeout is the default request timeout.
|
||||
DefaultRequestTimeout = time.Second * 30
|
||||
// DefaultConnectionTimeout is the default connection timeout.
|
||||
DefaultConnectionTimeout = time.Second * 5
|
||||
// DefaultPoolSize sets the connection pool size.
|
||||
DefaultPoolSize = 100
|
||||
// DefaultPoolTTL sets the connection pool ttl.
|
||||
DefaultPoolTTL = time.Minute
|
||||
)
|
||||
|
||||
// Options are the Client options.
|
||||
type Options struct {
|
||||
|
||||
// Default Call Options
|
||||
CallOptions CallOptions
|
||||
|
||||
// Router sets the router
|
||||
Router Router
|
||||
|
||||
Registry registry.Registry
|
||||
Selector selector.Selector
|
||||
Transport transport.Transport
|
||||
|
||||
// Plugged interfaces
|
||||
Broker broker.Broker
|
||||
|
||||
// Logger is the underline logger
|
||||
Logger logger.Logger
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
Codecs map[string]codec.NewCodec
|
||||
|
||||
// Response cache
|
||||
Cache *Cache
|
||||
|
||||
// Used to select codec
|
||||
ContentType string
|
||||
|
||||
// Middleware for client
|
||||
Wrappers []Wrapper
|
||||
|
||||
// Connection Pool
|
||||
PoolSize int
|
||||
PoolTTL time.Duration
|
||||
}
|
||||
|
||||
// CallOptions are options used to make calls to a server.
|
||||
type CallOptions struct {
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
// Backoff func
|
||||
Backoff BackoffFunc
|
||||
// Check if retriable func
|
||||
Retry RetryFunc
|
||||
SelectOptions []selector.SelectOption
|
||||
|
||||
// Address of remote hosts
|
||||
Address []string
|
||||
|
||||
// Middleware for low level call func
|
||||
CallWrappers []CallWrapper
|
||||
|
||||
// ConnectionTimeout of one request to the server.
|
||||
// Set this lower than the RequestTimeout to enbale retries on connection timeout.
|
||||
ConnectionTimeout time.Duration
|
||||
// Request/Response timeout of entire srv.Call, for single request timeout set ConnectionTimeout.
|
||||
RequestTimeout time.Duration
|
||||
// Stream timeout for the stream
|
||||
StreamTimeout time.Duration
|
||||
// Duration to cache the response for
|
||||
CacheExpiry time.Duration
|
||||
// Transport Dial Timeout. Used for initial dial to establish a connection.
|
||||
DialTimeout time.Duration
|
||||
// Number of Call attempts
|
||||
Retries int
|
||||
// Use the services own auth token
|
||||
ServiceToken bool
|
||||
// ConnClose sets the Connection: close header.
|
||||
ConnClose bool
|
||||
}
|
||||
|
||||
type PublishOptions struct {
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
// Exchange is the routing exchange for the message
|
||||
Exchange string
|
||||
}
|
||||
|
||||
type MessageOptions struct {
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type RequestOptions struct {
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
ContentType string
|
||||
Stream bool
|
||||
}
|
||||
|
||||
// NewOptions creates new Client options.
|
||||
func NewOptions(options ...Option) Options {
|
||||
opts := Options{
|
||||
Cache: NewCache(),
|
||||
Context: context.Background(),
|
||||
ContentType: DefaultContentType,
|
||||
Codecs: make(map[string]codec.NewCodec),
|
||||
CallOptions: CallOptions{
|
||||
Backoff: DefaultBackoff,
|
||||
Retry: DefaultRetry,
|
||||
Retries: DefaultRetries,
|
||||
RequestTimeout: DefaultRequestTimeout,
|
||||
ConnectionTimeout: DefaultConnectionTimeout,
|
||||
DialTimeout: transport.DefaultDialTimeout,
|
||||
},
|
||||
PoolSize: DefaultPoolSize,
|
||||
PoolTTL: DefaultPoolTTL,
|
||||
Broker: broker.DefaultBroker,
|
||||
Selector: selector.DefaultSelector,
|
||||
Registry: registry.DefaultRegistry,
|
||||
Transport: transport.DefaultTransport,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// Broker to be used for pub/sub.
|
||||
func Broker(b broker.Broker) Option {
|
||||
return func(o *Options) {
|
||||
o.Broker = b
|
||||
}
|
||||
}
|
||||
|
||||
// Codec to be used to encode/decode requests for a given content type.
|
||||
func Codec(contentType string, c codec.NewCodec) Option {
|
||||
return func(o *Options) {
|
||||
o.Codecs[contentType] = c
|
||||
}
|
||||
}
|
||||
|
||||
// ContentType sets the default content type of the client.
|
||||
func ContentType(ct string) Option {
|
||||
return func(o *Options) {
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
// PoolSize sets the connection pool size.
|
||||
func PoolSize(d int) Option {
|
||||
return func(o *Options) {
|
||||
o.PoolSize = d
|
||||
}
|
||||
}
|
||||
|
||||
// PoolTTL sets the connection pool ttl.
|
||||
func PoolTTL(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.PoolTTL = d
|
||||
}
|
||||
}
|
||||
|
||||
// Registry to find nodes for a given service.
|
||||
func Registry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
// set in the selector
|
||||
o.Selector.Init(selector.Registry(r))
|
||||
}
|
||||
}
|
||||
|
||||
// Transport to use for communication e.g http, rabbitmq, etc.
|
||||
func Transport(t transport.Transport) Option {
|
||||
return func(o *Options) {
|
||||
o.Transport = t
|
||||
}
|
||||
}
|
||||
|
||||
// Select is used to select a node to route a request to.
|
||||
func Selector(s selector.Selector) Option {
|
||||
return func(o *Options) {
|
||||
o.Selector = s
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a Wrapper to a list of options passed into the client.
|
||||
func Wrap(w Wrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.Wrappers = append(o.Wrappers, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a Wrapper to the list of CallFunc wrappers.
|
||||
func WrapCall(cw ...CallWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.CallWrappers = append(o.CallOptions.CallWrappers, cw...)
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff is used to set the backoff function used
|
||||
// when retrying Calls.
|
||||
func Backoff(fn BackoffFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.Backoff = fn
|
||||
}
|
||||
}
|
||||
|
||||
// Retries set the number of retries when making the request.
|
||||
func Retries(i int) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.Retries = i
|
||||
}
|
||||
}
|
||||
|
||||
// Retry sets the retry function to be used when re-trying.
|
||||
func Retry(fn RetryFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.Retry = fn
|
||||
}
|
||||
}
|
||||
|
||||
// RequestTimeout set the request timeout.
|
||||
func RequestTimeout(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.RequestTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// StreamTimeout sets the stream timeout.
|
||||
func StreamTimeout(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.StreamTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// DialTimeout sets the transport dial timeout.
|
||||
func DialTimeout(d time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.CallOptions.DialTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// Call Options
|
||||
|
||||
// WithExchange sets the exchange to route a message through.
|
||||
func WithExchange(e string) PublishOption {
|
||||
return func(o *PublishOptions) {
|
||||
o.Exchange = e
|
||||
}
|
||||
}
|
||||
|
||||
// PublishContext sets the context in publish options.
|
||||
func PublishContext(ctx context.Context) PublishOption {
|
||||
return func(o *PublishOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithAddress sets the remote addresses to use rather than using service discovery.
|
||||
func WithAddress(a ...string) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Address = a
|
||||
}
|
||||
}
|
||||
|
||||
func WithSelectOption(so ...selector.SelectOption) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.SelectOptions = append(o.SelectOptions, so...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCallWrapper is a CallOption which adds to the existing CallFunc wrappers.
|
||||
func WithCallWrapper(cw ...CallWrapper) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.CallWrappers = append(o.CallWrappers, cw...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithBackoff is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithBackoff(fn BackoffFunc) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Backoff = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetry is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithRetry(fn RetryFunc) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Retry = fn
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetries sets the number of tries for a call.
|
||||
// This CallOption overrides Options.CallOptions.
|
||||
func WithRetries(i int) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.Retries = i
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequestTimeout is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithRequestTimeout(d time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.RequestTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithConnClose sets the Connection header to close.
|
||||
func WithConnClose() CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.ConnClose = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithStreamTimeout sets the stream timeout.
|
||||
func WithStreamTimeout(d time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.StreamTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithDialTimeout is a CallOption which overrides that which
|
||||
// set in Options.CallOptions.
|
||||
func WithDialTimeout(d time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.DialTimeout = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithServiceToken is a CallOption which overrides the
|
||||
// authorization header with the services own auth token.
|
||||
func WithServiceToken() CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.ServiceToken = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithCache is a CallOption which sets the duration the response
|
||||
// shoull be cached for.
|
||||
func WithCache(c time.Duration) CallOption {
|
||||
return func(o *CallOptions) {
|
||||
o.CacheExpiry = c
|
||||
}
|
||||
}
|
||||
|
||||
func WithMessageContentType(ct string) MessageOption {
|
||||
return func(o *MessageOptions) {
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
// Request Options
|
||||
|
||||
func WithContentType(ct string) RequestOption {
|
||||
return func(o *RequestOptions) {
|
||||
o.ContentType = ct
|
||||
}
|
||||
}
|
||||
|
||||
func StreamingRequest() RequestOption {
|
||||
return func(o *RequestOptions) {
|
||||
o.Stream = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithRouter sets the client router.
|
||||
func WithRouter(r Router) Option {
|
||||
return func(o *Options) {
|
||||
o.Router = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the underline logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/errors"
|
||||
)
|
||||
|
||||
// note that returning either false or a non-nil error will result in the call not being retried.
|
||||
type RetryFunc func(ctx context.Context, req Request, retryCount int, err error) (bool, error)
|
||||
|
||||
// RetryAlways always retry on error.
|
||||
func RetryAlways(ctx context.Context, req Request, retryCount int, err error) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RetryOnError retries a request on a 500 or timeout error.
|
||||
func RetryOnError(ctx context.Context, req Request, retryCount int, err error) (bool, error) {
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
e := errors.Parse(err.Error())
|
||||
if e == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch e.Code {
|
||||
// Retry on timeout, not on 500 internal server error, as that is a business
|
||||
// logic error that should be handled by the user.
|
||||
case 408:
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
+736
@@ -0,0 +1,736 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
raw "go-micro.dev/v4/codec/bytes"
|
||||
merrors "go-micro.dev/v4/errors"
|
||||
log "go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
"go-micro.dev/v4/transport"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
"go-micro.dev/v4/util/buf"
|
||||
"go-micro.dev/v4/util/net"
|
||||
"go-micro.dev/v4/util/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
packageID = "go.micro.client"
|
||||
)
|
||||
|
||||
type rpcClient struct {
|
||||
opts Options
|
||||
once atomic.Value
|
||||
pool pool.Pool
|
||||
|
||||
seq uint64
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newRPCClient(opt ...Option) Client {
|
||||
opts := NewOptions(opt...)
|
||||
|
||||
p := pool.NewPool(
|
||||
pool.Size(opts.PoolSize),
|
||||
pool.TTL(opts.PoolTTL),
|
||||
pool.Transport(opts.Transport),
|
||||
)
|
||||
|
||||
rc := &rpcClient{
|
||||
opts: opts,
|
||||
pool: p,
|
||||
seq: 0,
|
||||
}
|
||||
rc.once.Store(false)
|
||||
|
||||
c := Client(rc)
|
||||
|
||||
// wrap in reverse
|
||||
for i := len(opts.Wrappers); i > 0; i-- {
|
||||
c = opts.Wrappers[i-1](c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *rpcClient) newCodec(contentType string) (codec.NewCodec, error) {
|
||||
if c, ok := r.opts.Codecs[contentType]; ok {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if cf, ok := DefaultCodecs[contentType]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
func (r *rpcClient) call(
|
||||
ctx context.Context,
|
||||
node *registry.Node,
|
||||
req Request,
|
||||
resp interface{},
|
||||
opts CallOptions,
|
||||
) error {
|
||||
address := node.Address
|
||||
logger := r.Options().Logger
|
||||
|
||||
msg := &transport.Message{
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if ok {
|
||||
for k, v := range md {
|
||||
// Don't copy Micro-Topic header, that is used for pub/sub
|
||||
// this is fixes the case when the client uses the same context that
|
||||
// is received in the subscriber.
|
||||
if k == headers.Message {
|
||||
continue
|
||||
}
|
||||
|
||||
msg.Header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Set connection timeout for single requests to the server. Should be > 0
|
||||
// as otherwise requests can't be made.
|
||||
cTimeout := opts.ConnectionTimeout
|
||||
if cTimeout == 0 {
|
||||
logger.Log(log.DebugLevel, "connection timeout was set to 0, overridng to default connection timeout")
|
||||
|
||||
cTimeout = DefaultConnectionTimeout
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
msg.Header["Timeout"] = fmt.Sprintf("%d", cTimeout)
|
||||
// set the content type for the request
|
||||
msg.Header["Content-Type"] = req.ContentType()
|
||||
// set the accept header
|
||||
msg.Header["Accept"] = req.ContentType()
|
||||
|
||||
// setup old protocol
|
||||
reqCodec := setupProtocol(msg, node)
|
||||
|
||||
// no codec specified
|
||||
if reqCodec == nil {
|
||||
var err error
|
||||
reqCodec, err = r.newCodec(req.ContentType())
|
||||
|
||||
if err != nil {
|
||||
return merrors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
dOpts := []transport.DialOption{
|
||||
transport.WithStream(),
|
||||
}
|
||||
|
||||
if opts.DialTimeout >= 0 {
|
||||
dOpts = append(dOpts, transport.WithTimeout(opts.DialTimeout))
|
||||
}
|
||||
|
||||
if opts.ConnClose {
|
||||
dOpts = append(dOpts, transport.WithConnClose())
|
||||
}
|
||||
|
||||
c, err := r.pool.Get(address, dOpts...)
|
||||
if err != nil {
|
||||
return merrors.InternalServerError("go.micro.client", "connection error: %v", err)
|
||||
}
|
||||
|
||||
seq := atomic.AddUint64(&r.seq, 1) - 1
|
||||
codec := newRPCCodec(msg, c, reqCodec, "")
|
||||
|
||||
rsp := &rpcResponse{
|
||||
socket: c,
|
||||
codec: codec,
|
||||
}
|
||||
|
||||
releaseFunc := func(err error) {
|
||||
if err = r.pool.Release(c, err); err != nil {
|
||||
logger.Log(log.ErrorLevel, "failed to release pool", err)
|
||||
}
|
||||
}
|
||||
|
||||
stream := &rpcStream{
|
||||
id: fmt.Sprintf("%v", seq),
|
||||
context: ctx,
|
||||
request: req,
|
||||
response: rsp,
|
||||
codec: codec,
|
||||
closed: make(chan bool),
|
||||
close: opts.ConnClose,
|
||||
release: releaseFunc,
|
||||
sendEOS: false,
|
||||
}
|
||||
|
||||
// close the stream on exiting this function
|
||||
defer func() {
|
||||
if err := stream.Close(); err != nil {
|
||||
logger.Log(log.ErrorLevel, "failed to close stream", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// wait for error response
|
||||
ch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ch <- merrors.InternalServerError("go.micro.client", "panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// send request
|
||||
if err := stream.Send(req.Body()); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
// recv response
|
||||
if err := stream.Recv(resp); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
// success
|
||||
ch <- nil
|
||||
}()
|
||||
|
||||
var grr error
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
case <-time.After(cTimeout):
|
||||
grr = merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
}
|
||||
|
||||
// set the stream error
|
||||
if grr != nil {
|
||||
stream.Lock()
|
||||
stream.err = grr
|
||||
stream.Unlock()
|
||||
|
||||
return grr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcClient) stream(ctx context.Context, node *registry.Node, req Request, opts CallOptions) (Stream, error) {
|
||||
address := node.Address
|
||||
logger := r.Options().Logger
|
||||
|
||||
msg := &transport.Message{
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if ok {
|
||||
for k, v := range md {
|
||||
msg.Header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
if opts.StreamTimeout > time.Duration(0) {
|
||||
msg.Header["Timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
|
||||
}
|
||||
// set the content type for the request
|
||||
msg.Header["Content-Type"] = req.ContentType()
|
||||
// set the accept header
|
||||
msg.Header["Accept"] = req.ContentType()
|
||||
|
||||
// set old codecs
|
||||
nCodec := setupProtocol(msg, node)
|
||||
|
||||
// no codec specified
|
||||
if nCodec == nil {
|
||||
var err error
|
||||
|
||||
nCodec, err = r.newCodec(req.ContentType())
|
||||
if err != nil {
|
||||
return nil, merrors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
dOpts := []transport.DialOption{
|
||||
transport.WithStream(),
|
||||
}
|
||||
|
||||
if opts.DialTimeout >= 0 {
|
||||
dOpts = append(dOpts, transport.WithTimeout(opts.DialTimeout))
|
||||
}
|
||||
|
||||
c, err := r.opts.Transport.Dial(address, dOpts...)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "connection error: %v", err)
|
||||
}
|
||||
|
||||
// increment the sequence number
|
||||
seq := atomic.AddUint64(&r.seq, 1) - 1
|
||||
id := fmt.Sprintf("%v", seq)
|
||||
|
||||
// create codec with stream id
|
||||
codec := newRPCCodec(msg, c, nCodec, id)
|
||||
|
||||
rsp := &rpcResponse{
|
||||
socket: c,
|
||||
codec: codec,
|
||||
}
|
||||
|
||||
// set request codec
|
||||
if r, ok := req.(*rpcRequest); ok {
|
||||
r.codec = codec
|
||||
}
|
||||
|
||||
stream := &rpcStream{
|
||||
id: id,
|
||||
context: ctx,
|
||||
request: req,
|
||||
response: rsp,
|
||||
codec: codec,
|
||||
// used to close the stream
|
||||
closed: make(chan bool),
|
||||
// signal the end of stream,
|
||||
sendEOS: true,
|
||||
release: func(_ error) {},
|
||||
}
|
||||
|
||||
// wait for error response
|
||||
ch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
// send the first message
|
||||
ch <- stream.Send(req.Body())
|
||||
}()
|
||||
|
||||
var grr error
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
grr = err
|
||||
case <-ctx.Done():
|
||||
grr = merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
}
|
||||
|
||||
if grr != nil {
|
||||
// set the error
|
||||
stream.Lock()
|
||||
stream.err = grr
|
||||
stream.Unlock()
|
||||
|
||||
// close the stream
|
||||
if err := stream.Close(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "failed to close stream: %v", err)
|
||||
}
|
||||
|
||||
return nil, grr
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (r *rpcClient) Init(opts ...Option) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
size := r.opts.PoolSize
|
||||
ttl := r.opts.PoolTTL
|
||||
tr := r.opts.Transport
|
||||
|
||||
for _, o := range opts {
|
||||
o(&r.opts)
|
||||
}
|
||||
|
||||
// update pool configuration if the options changed
|
||||
if size != r.opts.PoolSize || ttl != r.opts.PoolTTL || tr != r.opts.Transport {
|
||||
// close existing pool
|
||||
if err := r.pool.Close(); err != nil {
|
||||
return errors.Wrap(err, "failed to close pool")
|
||||
}
|
||||
|
||||
// create new pool
|
||||
r.pool = pool.NewPool(
|
||||
pool.Size(r.opts.PoolSize),
|
||||
pool.TTL(r.opts.PoolTTL),
|
||||
pool.Transport(r.opts.Transport),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options retrives the options.
|
||||
func (r *rpcClient) Options() Options {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
return r.opts
|
||||
}
|
||||
|
||||
// next returns an iterator for the next nodes to call.
|
||||
func (r *rpcClient) next(request Request, opts CallOptions) (selector.Next, error) {
|
||||
// try get the proxy
|
||||
service, address, _ := net.Proxy(request.Service(), opts.Address)
|
||||
|
||||
// return remote address
|
||||
if len(address) > 0 {
|
||||
nodes := make([]*registry.Node, len(address))
|
||||
|
||||
for i, addr := range address {
|
||||
nodes[i] = ®istry.Node{
|
||||
Address: addr,
|
||||
// Set the protocol
|
||||
Metadata: map[string]string{
|
||||
"protocol": "mucp",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// crude return method
|
||||
return func() (*registry.Node, error) {
|
||||
return nodes[time.Now().Unix()%int64(len(nodes))], nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// get next nodes from the selector
|
||||
next, err := r.opts.Selector.Select(service, opts.SelectOptions...)
|
||||
if err != nil {
|
||||
if errors.Is(err, selector.ErrNotFound) {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return nil, merrors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (r *rpcClient) Call(ctx context.Context, request Request, response interface{}, opts ...CallOption) error {
|
||||
// TODO: further validate these mutex locks. full lock would prevent
|
||||
// parallel calls. Maybe we can set individual locks for secctions.
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// make a copy of call opts
|
||||
callOpts := r.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
next, err := r.next(request, callOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if we already have a deadline
|
||||
d, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
// no deadline so we create a new one
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
|
||||
|
||||
defer cancel()
|
||||
} else {
|
||||
// got a deadline so no need to setup context
|
||||
// but we need to set the timeout we pass along
|
||||
opt := WithRequestTimeout(time.Until(d))
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// should we noop right here?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
default:
|
||||
}
|
||||
|
||||
// make copy of call method
|
||||
rcall := r.call
|
||||
|
||||
// wrap the call in reverse
|
||||
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||
rcall = callOpts.CallWrappers[i-1](rcall)
|
||||
}
|
||||
|
||||
// return errors.New("go.micro.client", "request timeout", 408)
|
||||
call := func(i int) error {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, request, i)
|
||||
if err != nil {
|
||||
return merrors.InternalServerError("go.micro.client", "backoff error: %v", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
// select next node
|
||||
node, err := next()
|
||||
service := request.Service()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, selector.ErrNotFound) {
|
||||
return merrors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return merrors.InternalServerError("go.micro.client",
|
||||
"error getting next %s node: %s",
|
||||
service,
|
||||
err.Error())
|
||||
}
|
||||
|
||||
// make the call
|
||||
err = rcall(ctx, node, request, response, callOpts)
|
||||
r.opts.Selector.Mark(service, node, err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// get the retries
|
||||
retries := callOpts.Retries
|
||||
|
||||
// disable retries when using a proxy
|
||||
// Note: I don't see why we should disable retries for proxies, so commenting out.
|
||||
// if _, _, ok := net.Proxy(request.Service(), callOpts.Address); ok {
|
||||
// retries = 0
|
||||
// }
|
||||
|
||||
ch := make(chan error, retries+1)
|
||||
|
||||
var gerr error
|
||||
|
||||
for i := 0; i <= retries; i++ {
|
||||
go func(i int) {
|
||||
ch <- call(i)
|
||||
}(i)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return merrors.Timeout("go.micro.client", fmt.Sprintf("call timeout: %v", ctx.Err()))
|
||||
case err := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, request, i, err)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return err
|
||||
}
|
||||
|
||||
r.opts.Logger.Logf(log.DebugLevel, "Retrying request. Previous attempt failed with: %v", err)
|
||||
|
||||
gerr = err
|
||||
}
|
||||
}
|
||||
|
||||
return gerr
|
||||
}
|
||||
|
||||
func (r *rpcClient) Stream(ctx context.Context, request Request, opts ...CallOption) (Stream, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// make a copy of call opts
|
||||
callOpts := r.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
next, err := r.next(request, callOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, merrors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
|
||||
default:
|
||||
}
|
||||
|
||||
call := func(i int) (Stream, error) {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, request, i)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "backoff error: %v", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
node, err := next()
|
||||
service := request.Service()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, selector.ErrNotFound) {
|
||||
return nil, merrors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return nil, merrors.InternalServerError("go.micro.client",
|
||||
"error getting next %s node: %s",
|
||||
service,
|
||||
err.Error())
|
||||
}
|
||||
|
||||
stream, err := r.stream(ctx, node, request, callOpts)
|
||||
r.opts.Selector.Mark(service, node, err)
|
||||
|
||||
return stream, err
|
||||
}
|
||||
|
||||
type response struct {
|
||||
stream Stream
|
||||
err error
|
||||
}
|
||||
|
||||
// get the retries
|
||||
retries := callOpts.Retries
|
||||
|
||||
// disable retries when using a proxy
|
||||
if _, _, ok := net.Proxy(request.Service(), callOpts.Address); ok {
|
||||
retries = 0
|
||||
}
|
||||
|
||||
ch := make(chan response, retries+1)
|
||||
|
||||
var grr error
|
||||
|
||||
for i := 0; i <= retries; i++ {
|
||||
go func(i int) {
|
||||
s, err := call(i)
|
||||
ch <- response{s, err}
|
||||
}(i)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, merrors.Timeout("go.micro.client", fmt.Sprintf("call timeout: %v", ctx.Err()))
|
||||
case rsp := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if rsp.err == nil {
|
||||
return rsp.stream, nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, request, i, rsp.err)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return nil, rsp.err
|
||||
}
|
||||
|
||||
grr = rsp.err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, grr
|
||||
}
|
||||
|
||||
func (r *rpcClient) Publish(ctx context.Context, msg Message, opts ...PublishOption) error {
|
||||
options := PublishOptions{
|
||||
Context: context.Background(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
metadata, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
metadata = make(map[string]string)
|
||||
}
|
||||
|
||||
id := uuid.New().String()
|
||||
metadata["Content-Type"] = msg.ContentType()
|
||||
metadata[headers.Message] = msg.Topic()
|
||||
metadata[headers.ID] = id
|
||||
|
||||
// set the topic
|
||||
topic := msg.Topic()
|
||||
|
||||
// get the exchange
|
||||
if len(options.Exchange) > 0 {
|
||||
topic = options.Exchange
|
||||
}
|
||||
|
||||
// encode message body
|
||||
cf, err := r.newCodec(msg.ContentType())
|
||||
if err != nil {
|
||||
return merrors.InternalServerError(packageID, err.Error())
|
||||
}
|
||||
|
||||
var body []byte
|
||||
|
||||
// passed in raw data
|
||||
if d, ok := msg.Payload().(*raw.Frame); ok {
|
||||
body = d.Data
|
||||
} else {
|
||||
b := buf.New(nil)
|
||||
|
||||
if err = cf(b).Write(&codec.Message{
|
||||
Target: topic,
|
||||
Type: codec.Event,
|
||||
Header: map[string]string{
|
||||
headers.ID: id,
|
||||
headers.Message: msg.Topic(),
|
||||
},
|
||||
}, msg.Payload()); err != nil {
|
||||
return merrors.InternalServerError(packageID, err.Error())
|
||||
}
|
||||
|
||||
// set the body
|
||||
body = b.Bytes()
|
||||
}
|
||||
|
||||
l, ok := r.once.Load().(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to cast to bool")
|
||||
}
|
||||
|
||||
if !l {
|
||||
if err = r.opts.Broker.Connect(); err != nil {
|
||||
return merrors.InternalServerError(packageID, err.Error())
|
||||
}
|
||||
|
||||
r.once.Store(true)
|
||||
}
|
||||
|
||||
return r.opts.Broker.Publish(topic, &broker.Message{
|
||||
Header: metadata,
|
||||
Body: body,
|
||||
}, broker.PublishContext(options.Context))
|
||||
}
|
||||
|
||||
func (r *rpcClient) NewMessage(topic string, message interface{}, opts ...MessageOption) Message {
|
||||
return newMessage(topic, message, r.opts.ContentType, opts...)
|
||||
}
|
||||
|
||||
func (r *rpcClient) NewRequest(service, method string, request interface{}, reqOpts ...RequestOption) Request {
|
||||
return newRequest(service, method, request, r.opts.ContentType, reqOpts...)
|
||||
}
|
||||
|
||||
func (r *rpcClient) String() string {
|
||||
return "mucp"
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
errs "errors"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
raw "go-micro.dev/v4/codec/bytes"
|
||||
"go-micro.dev/v4/codec/grpc"
|
||||
"go-micro.dev/v4/codec/json"
|
||||
"go-micro.dev/v4/codec/jsonrpc"
|
||||
"go-micro.dev/v4/codec/proto"
|
||||
"go-micro.dev/v4/codec/protorpc"
|
||||
"go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/transport"
|
||||
"go-micro.dev/v4/transport/headers"
|
||||
)
|
||||
|
||||
const (
|
||||
lastStreamResponseError = "EOS"
|
||||
)
|
||||
|
||||
// serverError represents an error that has been returned from
|
||||
// the remote side of the RPC connection.
|
||||
type serverError string
|
||||
|
||||
func (e serverError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// errShutdown holds the specific error for closing/closed connections.
|
||||
var (
|
||||
errShutdown = errs.New("connection is shut down")
|
||||
)
|
||||
|
||||
type rpcCodec struct {
|
||||
client transport.Client
|
||||
codec codec.Codec
|
||||
|
||||
req *transport.Message
|
||||
buf *readWriteCloser
|
||||
|
||||
// signify if its a stream
|
||||
stream string
|
||||
}
|
||||
|
||||
type readWriteCloser struct {
|
||||
wbuf *bytes.Buffer
|
||||
rbuf *bytes.Buffer
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultContentType header.
|
||||
DefaultContentType = "application/json"
|
||||
|
||||
// DefaultCodecs map.
|
||||
DefaultCodecs = map[string]codec.NewCodec{
|
||||
"application/grpc": grpc.NewCodec,
|
||||
"application/grpc+json": grpc.NewCodec,
|
||||
"application/grpc+proto": grpc.NewCodec,
|
||||
"application/protobuf": proto.NewCodec,
|
||||
"application/json": json.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/proto-rpc": protorpc.NewCodec,
|
||||
"application/octet-stream": raw.NewCodec,
|
||||
}
|
||||
|
||||
// TODO: remove legacy codec list.
|
||||
defaultCodecs = map[string]codec.NewCodec{
|
||||
"application/json": jsonrpc.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/protobuf": protorpc.NewCodec,
|
||||
"application/proto-rpc": protorpc.NewCodec,
|
||||
"application/octet-stream": protorpc.NewCodec,
|
||||
}
|
||||
)
|
||||
|
||||
func (rwc *readWriteCloser) Read(p []byte) (n int, err error) {
|
||||
return rwc.rbuf.Read(p)
|
||||
}
|
||||
|
||||
func (rwc *readWriteCloser) Write(p []byte) (n int, err error) {
|
||||
return rwc.wbuf.Write(p)
|
||||
}
|
||||
|
||||
func (rwc *readWriteCloser) Close() error {
|
||||
rwc.rbuf.Reset()
|
||||
rwc.wbuf.Reset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHeaders(m *codec.Message) {
|
||||
set := func(v, hdr string) string {
|
||||
if len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
|
||||
return m.Header[hdr]
|
||||
}
|
||||
|
||||
// check error in header
|
||||
m.Error = set(m.Error, headers.Error)
|
||||
|
||||
// check endpoint in header
|
||||
m.Endpoint = set(m.Endpoint, headers.Endpoint)
|
||||
|
||||
// check method in header
|
||||
m.Method = set(m.Method, headers.Method)
|
||||
|
||||
// set the request id
|
||||
m.Id = set(m.Id, headers.ID)
|
||||
}
|
||||
|
||||
func setHeaders(m *codec.Message, stream string) {
|
||||
set := func(hdr, v string) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.Header[hdr] = v
|
||||
}
|
||||
|
||||
set(headers.ID, m.Id)
|
||||
set(headers.Request, m.Target)
|
||||
set(headers.Method, m.Method)
|
||||
set(headers.Endpoint, m.Endpoint)
|
||||
set(headers.Error, m.Error)
|
||||
|
||||
if len(stream) > 0 {
|
||||
set(headers.Stream, stream)
|
||||
}
|
||||
}
|
||||
|
||||
// setupProtocol sets up the old protocol.
|
||||
func setupProtocol(msg *transport.Message, node *registry.Node) codec.NewCodec {
|
||||
protocol := node.Metadata["protocol"]
|
||||
|
||||
// got protocol
|
||||
if len(protocol) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// processing topic publishing
|
||||
if len(msg.Header[headers.Message]) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// no protocol use old codecs
|
||||
switch msg.Header["Content-Type"] {
|
||||
case "application/json":
|
||||
msg.Header["Content-Type"] = "application/json-rpc"
|
||||
case "application/protobuf":
|
||||
msg.Header["Content-Type"] = "application/proto-rpc"
|
||||
}
|
||||
|
||||
return defaultCodecs[msg.Header["Content-Type"]]
|
||||
}
|
||||
|
||||
func newRPCCodec(req *transport.Message, client transport.Client, c codec.NewCodec, stream string) codec.Codec {
|
||||
rwc := &readWriteCloser{
|
||||
wbuf: bytes.NewBuffer(nil),
|
||||
rbuf: bytes.NewBuffer(nil),
|
||||
}
|
||||
|
||||
return &rpcCodec{
|
||||
buf: rwc,
|
||||
client: client,
|
||||
codec: c(rwc),
|
||||
req: req,
|
||||
stream: stream,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rpcCodec) Write(message *codec.Message, body interface{}) error {
|
||||
c.buf.wbuf.Reset()
|
||||
|
||||
// create header
|
||||
if message.Header == nil {
|
||||
message.Header = map[string]string{}
|
||||
}
|
||||
|
||||
// copy original header
|
||||
for k, v := range c.req.Header {
|
||||
message.Header[k] = v
|
||||
}
|
||||
|
||||
// set the mucp headers
|
||||
setHeaders(message, c.stream)
|
||||
|
||||
// if body is bytes Frame don't encode
|
||||
if body != nil {
|
||||
if b, ok := body.(*raw.Frame); ok {
|
||||
// set body
|
||||
message.Body = b.Data
|
||||
} else {
|
||||
// write to codec
|
||||
if err := c.codec.Write(message, body); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.codec", err.Error())
|
||||
}
|
||||
// set body
|
||||
message.Body = c.buf.wbuf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
// create new transport message
|
||||
msg := transport.Message{
|
||||
Header: message.Header,
|
||||
Body: message.Body,
|
||||
}
|
||||
|
||||
// send the request
|
||||
if err := c.client.Send(&msg); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.transport", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) ReadHeader(msg *codec.Message, r codec.MessageType) error {
|
||||
var tm transport.Message
|
||||
|
||||
// read message from transport
|
||||
if err := c.client.Recv(&tm); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.transport", err.Error())
|
||||
}
|
||||
|
||||
c.buf.rbuf.Reset()
|
||||
c.buf.rbuf.Write(tm.Body)
|
||||
|
||||
// set headers from transport
|
||||
msg.Header = tm.Header
|
||||
|
||||
// read header
|
||||
err := c.codec.ReadHeader(msg, r)
|
||||
|
||||
// get headers
|
||||
getHeaders(msg)
|
||||
|
||||
// return header error
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client.codec", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) ReadBody(b interface{}) error {
|
||||
// read body
|
||||
// read raw data
|
||||
if v, ok := b.(*raw.Frame); ok {
|
||||
v.Data = c.buf.rbuf.Bytes()
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.codec.ReadBody(b); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.codec", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) Close() error {
|
||||
if err := c.buf.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.codec.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.client.Close(); err != nil {
|
||||
return errors.InternalServerError("go.micro.client.transport", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) String() string {
|
||||
return "rpc"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package client
|
||||
|
||||
type message struct {
|
||||
payload interface{}
|
||||
topic string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func newMessage(topic string, payload interface{}, contentType string, opts ...MessageOption) Message {
|
||||
var options MessageOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if len(options.ContentType) > 0 {
|
||||
contentType = options.ContentType
|
||||
}
|
||||
|
||||
return &message{
|
||||
payload: payload,
|
||||
topic: topic,
|
||||
contentType: contentType,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *message) ContentType() string {
|
||||
return m.contentType
|
||||
}
|
||||
|
||||
func (m *message) Topic() string {
|
||||
return m.topic
|
||||
}
|
||||
|
||||
func (m *message) Payload() interface{} {
|
||||
return m.payload
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type rpcRequest struct {
|
||||
opts RequestOptions
|
||||
codec codec.Codec
|
||||
body interface{}
|
||||
service string
|
||||
method string
|
||||
endpoint string
|
||||
contentType string
|
||||
}
|
||||
|
||||
func newRequest(service, endpoint string, request interface{}, contentType string, reqOpts ...RequestOption) Request {
|
||||
var opts RequestOptions
|
||||
|
||||
for _, o := range reqOpts {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
// set the content-type specified
|
||||
if len(opts.ContentType) > 0 {
|
||||
contentType = opts.ContentType
|
||||
}
|
||||
|
||||
return &rpcRequest{
|
||||
service: service,
|
||||
method: endpoint,
|
||||
endpoint: endpoint,
|
||||
body: request,
|
||||
contentType: contentType,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcRequest) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Service() string {
|
||||
return r.service
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Method() string {
|
||||
return r.method
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Endpoint() string {
|
||||
return r.endpoint
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Body() interface{} {
|
||||
return r.body
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Codec() codec.Writer {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Stream() bool {
|
||||
return r.opts.Stream
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/transport"
|
||||
)
|
||||
|
||||
type rpcResponse struct {
|
||||
socket transport.Socket
|
||||
codec codec.Codec
|
||||
header map[string]string
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Codec() codec.Reader {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Header() map[string]string {
|
||||
return r.header
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Read() ([]byte, error) {
|
||||
var msg transport.Message
|
||||
|
||||
if err := r.socket.Recv(&msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set internals
|
||||
r.header = msg.Header
|
||||
r.body = msg.Body
|
||||
|
||||
return msg.Body, nil
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
// Implements the streamer interface.
|
||||
type rpcStream struct {
|
||||
err error
|
||||
request Request
|
||||
response Response
|
||||
codec codec.Codec
|
||||
context context.Context
|
||||
|
||||
closed chan bool
|
||||
|
||||
// release releases the connection back to the pool
|
||||
release func(err error)
|
||||
id string
|
||||
sync.RWMutex
|
||||
// Indicates whether connection should be closed directly.
|
||||
close bool
|
||||
|
||||
// signal whether we should send EOS
|
||||
sendEOS bool
|
||||
}
|
||||
|
||||
func (r *rpcStream) isClosed() bool {
|
||||
select {
|
||||
case <-r.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcStream) Context() context.Context {
|
||||
return r.context
|
||||
}
|
||||
|
||||
func (r *rpcStream) Request() Request {
|
||||
return r.request
|
||||
}
|
||||
|
||||
func (r *rpcStream) Response() Response {
|
||||
return r.response
|
||||
}
|
||||
|
||||
func (r *rpcStream) Send(msg interface{}) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.isClosed() {
|
||||
r.err = errShutdown
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
req := codec.Message{
|
||||
Id: r.id,
|
||||
Target: r.request.Service(),
|
||||
Method: r.request.Method(),
|
||||
Endpoint: r.request.Endpoint(),
|
||||
Type: codec.Request,
|
||||
}
|
||||
|
||||
if err := r.codec.Write(&req, msg); err != nil {
|
||||
r.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcStream) Recv(msg interface{}) error {
|
||||
r.Lock()
|
||||
|
||||
if r.isClosed() {
|
||||
r.err = errShutdown
|
||||
r.Unlock()
|
||||
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
var resp codec.Message
|
||||
|
||||
r.Unlock()
|
||||
err := r.codec.ReadHeader(&resp, codec.Response)
|
||||
r.Lock()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) && !r.isClosed() {
|
||||
r.err = io.ErrUnexpectedEOF
|
||||
r.Unlock()
|
||||
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
r.err = err
|
||||
|
||||
r.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(resp.Error) > 0:
|
||||
// We've got an error response. Give this to the request;
|
||||
// any subsequent requests will get the ReadResponseBody
|
||||
// error if there is one.
|
||||
if resp.Error != lastStreamResponseError {
|
||||
r.err = serverError(resp.Error)
|
||||
} else {
|
||||
r.err = io.EOF
|
||||
}
|
||||
r.Unlock()
|
||||
err = r.codec.ReadBody(nil)
|
||||
r.Lock()
|
||||
if err != nil {
|
||||
r.err = err
|
||||
}
|
||||
default:
|
||||
r.Unlock()
|
||||
err = r.codec.ReadBody(msg)
|
||||
r.Lock()
|
||||
if err != nil {
|
||||
r.err = err
|
||||
}
|
||||
}
|
||||
|
||||
defer r.Unlock()
|
||||
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *rpcStream) Error() error {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *rpcStream) CloseSend() error {
|
||||
return errors.New("streamer not implemented")
|
||||
}
|
||||
|
||||
func (r *rpcStream) Close() error {
|
||||
r.Lock()
|
||||
|
||||
select {
|
||||
case <-r.closed:
|
||||
r.Unlock()
|
||||
return nil
|
||||
default:
|
||||
close(r.closed)
|
||||
r.Unlock()
|
||||
|
||||
// send the end of stream message
|
||||
if r.sendEOS {
|
||||
// no need to check for error
|
||||
//nolint:errcheck,gosec
|
||||
r.codec.Write(&codec.Message{
|
||||
Id: r.id,
|
||||
Target: r.request.Service(),
|
||||
Method: r.request.Method(),
|
||||
Endpoint: r.request.Endpoint(),
|
||||
Type: codec.Error,
|
||||
Error: lastStreamResponseError,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
err := r.codec.Close()
|
||||
|
||||
rerr := r.Error()
|
||||
if r.close && rerr == nil {
|
||||
rerr = errors.New("connection header set to close")
|
||||
}
|
||||
// release the connection
|
||||
r.release(rerr)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
// CallFunc represents the individual call func.
|
||||
type CallFunc func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error
|
||||
|
||||
// CallWrapper is a low level wrapper for the CallFunc.
|
||||
type CallWrapper func(CallFunc) CallFunc
|
||||
|
||||
// Wrapper wraps a client and returns a client.
|
||||
type Wrapper func(Client) Client
|
||||
|
||||
// StreamWrapper wraps a Stream and returns the equivalent.
|
||||
type StreamWrapper func(Stream) Stream
|
||||
Reference in New Issue
Block a user