Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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] = &registry.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
View File
@@ -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"
}
+36
View File
@@ -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
}
+65
View File
@@ -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
}
+35
View File
@@ -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
View File
@@ -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
View File
@@ -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