Compare commits
1 Commits
v4.0.4
...
d660c085b2
Author | SHA1 | Date | |
---|---|---|---|
d660c085b2 |
@@ -10,15 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// DefaultBroker default memory broker
|
// DefaultBroker default memory broker
|
||||||
var DefaultBroker Broker // = NewBroker()
|
var DefaultBroker = NewBroker()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNotConnected returns when broker used but not connected yet
|
// ErrNotConnected returns when broker used but not connected yet
|
||||||
ErrNotConnected = errors.New("broker not connected")
|
ErrNotConnected = errors.New("broker not connected")
|
||||||
// ErrDisconnected returns when broker disconnected
|
// ErrDisconnected returns when broker disconnected
|
||||||
ErrDisconnected = errors.New("broker disconnected")
|
ErrDisconnected = errors.New("broker disconnected")
|
||||||
// ErrInvalidMessage returns when message has nvalid format
|
|
||||||
ErrInvalidMessage = errors.New("broker message has invalid format")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Broker is an interface used for asynchronous messaging.
|
// Broker is an interface used for asynchronous messaging.
|
||||||
@@ -35,33 +33,61 @@ type Broker interface {
|
|||||||
Connect(ctx context.Context) error
|
Connect(ctx context.Context) error
|
||||||
// Disconnect disconnect from broker
|
// Disconnect disconnect from broker
|
||||||
Disconnect(ctx context.Context) error
|
Disconnect(ctx context.Context) error
|
||||||
// NewMessage creates new broker message
|
|
||||||
NewMessage(endpoint string, req interface{}, opts ...MessageOption) Message
|
|
||||||
// Publish message to broker topic
|
// Publish message to broker topic
|
||||||
Publish(ctx context.Context, msg interface{}, opts ...PublishOption) error
|
Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error
|
||||||
// Subscribe subscribes to topic message via handler
|
// Subscribe subscribes to topic message via handler
|
||||||
Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error)
|
Subscribe(ctx context.Context, topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
|
||||||
|
// BatchPublish messages to broker with multiple topics
|
||||||
|
BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error
|
||||||
|
// BatchSubscribe subscribes to topic messages via handler
|
||||||
|
BatchSubscribe(ctx context.Context, topic string, h BatchHandler, opts ...SubscribeOption) (Subscriber, error)
|
||||||
// String type of broker
|
// String type of broker
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message is given to a subscription handler for processing
|
// Handler is used to process messages via a subscription of a topic.
|
||||||
type Message interface {
|
type Handler func(Event) error
|
||||||
// Context for the message
|
|
||||||
Context() context.Context
|
// Events contains multiple events
|
||||||
|
type Events []Event
|
||||||
|
|
||||||
|
// Ack try to ack all events and return
|
||||||
|
func (evs Events) Ack() error {
|
||||||
|
var err error
|
||||||
|
for _, ev := range evs {
|
||||||
|
if err = ev.Ack(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError sets error on event
|
||||||
|
func (evs Events) SetError(err error) {
|
||||||
|
for _, ev := range evs {
|
||||||
|
ev.SetError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchHandler is used to process messages in batches via a subscription of a topic.
|
||||||
|
type BatchHandler func(Events) error
|
||||||
|
|
||||||
|
// Event is given to a subscription handler for processing
|
||||||
|
type Event interface {
|
||||||
// Topic returns event topic
|
// Topic returns event topic
|
||||||
Topic() string
|
Topic() string
|
||||||
// Body returns broker message
|
// Message returns broker message
|
||||||
Body() interface{}
|
Message() *Message
|
||||||
// Ack acknowledge message
|
// Ack acknowledge message
|
||||||
Ack() error
|
Ack() error
|
||||||
// Error returns message error (like decoding errors or some other)
|
// Error returns message error (like decoding errors or some other)
|
||||||
// In this case Body contains raw []byte from broker
|
|
||||||
Error() error
|
Error() error
|
||||||
|
// SetError set event processing error
|
||||||
|
SetError(err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawMessage is used to transfer data
|
// Message is used to transfer data
|
||||||
type RawMessage struct {
|
type Message struct {
|
||||||
// Header contains message metadata
|
// Header contains message metadata
|
||||||
Header metadata.Metadata
|
Header metadata.Metadata
|
||||||
// Body contains message body
|
// Body contains message body
|
||||||
@@ -69,8 +95,8 @@ type RawMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMessage create broker message with topic filled
|
// NewMessage create broker message with topic filled
|
||||||
func NewRawMessage(topic string) *RawMessage {
|
func NewMessage(topic string) *Message {
|
||||||
m := &RawMessage{Header: metadata.New(2)}
|
m := &Message{Header: metadata.New(2)}
|
||||||
m.Header.Set(metadata.HeaderTopic, topic)
|
m.Header.Set(metadata.HeaderTopic, topic)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
229
broker/memory.go
229
broker/memory.go
@@ -1,13 +1,11 @@
|
|||||||
//go:build ignore
|
|
||||||
|
|
||||||
package broker
|
package broker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
maddr "go.unistack.org/micro/v4/util/addr"
|
maddr "go.unistack.org/micro/v4/util/addr"
|
||||||
"go.unistack.org/micro/v4/util/id"
|
"go.unistack.org/micro/v4/util/id"
|
||||||
mnet "go.unistack.org/micro/v4/util/net"
|
mnet "go.unistack.org/micro/v4/util/net"
|
||||||
@@ -22,6 +20,23 @@ type memoryBroker struct {
|
|||||||
connected bool
|
connected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type memoryEvent struct {
|
||||||
|
err error
|
||||||
|
message interface{}
|
||||||
|
topic string
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
type memorySubscriber struct {
|
||||||
|
ctx context.Context
|
||||||
|
exit chan bool
|
||||||
|
handler Handler
|
||||||
|
batchhandler BatchHandler
|
||||||
|
id string
|
||||||
|
topic string
|
||||||
|
opts SubscribeOptions
|
||||||
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) Options() Options {
|
func (m *memoryBroker) Options() Options {
|
||||||
return m.opts
|
return m.opts
|
||||||
}
|
}
|
||||||
@@ -73,11 +88,16 @@ func (m *memoryBroker) Init(opts ...Option) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) NewMessage(endpoint string, req interface{}, opts ...MessageOption) Message {
|
func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
|
||||||
return &memoryMessage{}
|
msg.Header.Set(metadata.HeaderTopic, topic)
|
||||||
|
return m.publish(ctx, []*Message{msg}, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) Publish(ctx context.Context, message interface{}, opts ...PublishOption) error {
|
func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
||||||
|
return m.publish(ctx, msgs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) publish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
if !m.connected {
|
if !m.connected {
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
@@ -92,94 +112,128 @@ func (m *memoryBroker) Publish(ctx context.Context, message interface{}, opts ..
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
options := NewPublishOptions(opts...)
|
options := NewPublishOptions(opts...)
|
||||||
var msgs []*memoryMessage
|
|
||||||
switch v := message.(type) {
|
msgTopicMap := make(map[string]Events)
|
||||||
case *memoryMessage:
|
for _, v := range msgs {
|
||||||
msgs = []*memoryMessage{v}
|
p := &memoryEvent{opts: m.opts}
|
||||||
case []*memoryMessage:
|
|
||||||
msgs = v
|
if m.opts.Codec == nil || options.BodyOnly {
|
||||||
default:
|
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
||||||
return ErrInvalidMessage
|
p.message = v.Body
|
||||||
}
|
} else {
|
||||||
msgTopicMap := make(map[string][]*memoryMessage)
|
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
||||||
for _, msg := range msgs {
|
p.message, err = m.opts.Codec.Marshal(v)
|
||||||
p := &memoryMessage{opts: options}
|
if err != nil {
|
||||||
/*
|
return err
|
||||||
if mb, ok := msg.Body().(*codec.Frame); ok {
|
|
||||||
p.message = v.Body
|
|
||||||
} else {
|
|
||||||
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
|
|
||||||
p.message, err = m.opts.Codec.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
msgTopicMap[msg.Topic()] = append(msgTopicMap[p.topic], p)
|
msgTopicMap[p.topic] = append(msgTopicMap[p.topic], p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beh := m.opts.BatchErrorHandler
|
||||||
eh := m.opts.ErrorHandler
|
eh := m.opts.ErrorHandler
|
||||||
|
|
||||||
for t, ms := range msgTopicMap {
|
for t, ms := range msgTopicMap {
|
||||||
ts := time.Now()
|
|
||||||
|
|
||||||
m.opts.Meter.Counter(PublishMessageInflight, "endpoint", t).Add(len(ms))
|
|
||||||
m.opts.Meter.Counter(SubscribeMessageInflight, "endpoint", t).Add(len(ms))
|
|
||||||
|
|
||||||
m.RLock()
|
m.RLock()
|
||||||
subs, ok := m.subscribers[t]
|
subs, ok := m.subscribers[t]
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
m.opts.Meter.Counter(PublishMessageTotal, "endpoint", t, "status", "failure").Add(len(ms))
|
|
||||||
m.opts.Meter.Counter(PublishMessageInflight, "endpoint", t).Add(-len(ms))
|
|
||||||
m.opts.Meter.Counter(SubscribeMessageInflight, "endpoint", t).Add(-len(ms))
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
m.opts.Meter.Counter(PublishMessageTotal, "endpoint", t, "status", "success").Add(len(ms))
|
|
||||||
for _, sub := range subs {
|
for _, sub := range subs {
|
||||||
|
if sub.opts.BatchErrorHandler != nil {
|
||||||
|
beh = sub.opts.BatchErrorHandler
|
||||||
|
}
|
||||||
if sub.opts.ErrorHandler != nil {
|
if sub.opts.ErrorHandler != nil {
|
||||||
eh = sub.opts.ErrorHandler
|
eh = sub.opts.ErrorHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range ms {
|
switch {
|
||||||
if err = sub.handler(p); err != nil {
|
// batch processing
|
||||||
m.opts.Meter.Counter(SubscribeMessageTotal, "endpoint", t, "status", "failure").Inc()
|
case sub.batchhandler != nil:
|
||||||
if eh != nil {
|
if err = sub.batchhandler(ms); err != nil {
|
||||||
_ = eh(p)
|
ms.SetError(err)
|
||||||
|
if beh != nil {
|
||||||
|
_ = beh(ms)
|
||||||
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
m.opts.Logger.Error(m.opts.Context, err.Error())
|
m.opts.Logger.Error(m.opts.Context, err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else if sub.opts.AutoAck {
|
||||||
if sub.opts.AutoAck {
|
if err = ms.Ack(); err != nil {
|
||||||
if err = p.Ack(); err != nil {
|
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
||||||
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
}
|
||||||
m.opts.Meter.Counter(SubscribeMessageTotal, "endpoint", t, "status", "failure").Inc()
|
}
|
||||||
} else {
|
// single processing
|
||||||
m.opts.Meter.Counter(SubscribeMessageTotal, "endpoint", t, "status", "success").Inc()
|
case sub.handler != nil:
|
||||||
}
|
for _, p := range ms {
|
||||||
} else {
|
if err = sub.handler(p); err != nil {
|
||||||
m.opts.Meter.Counter(SubscribeMessageTotal, "endpoint", t, "status", "success").Inc()
|
p.SetError(err)
|
||||||
|
if eh != nil {
|
||||||
|
_ = eh(p)
|
||||||
|
} else if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, err.Error())
|
||||||
|
}
|
||||||
|
} else if sub.opts.AutoAck {
|
||||||
|
if err = p.Ack(); err != nil {
|
||||||
|
m.opts.Logger.Errorf(m.opts.Context, "ack failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.opts.Meter.Counter(PublishMessageInflight, "endpoint", t).Add(-1)
|
|
||||||
m.opts.Meter.Counter(SubscribeMessageInflight, "endpoint", t).Add(-1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
te := time.Since(ts)
|
|
||||||
m.opts.Meter.Summary(PublishMessageLatencyMicroseconds, "endpoint", t).Update(te.Seconds())
|
|
||||||
m.opts.Meter.Histogram(PublishMessageDurationSeconds, "endpoint", t).Update(te.Seconds())
|
|
||||||
m.opts.Meter.Summary(SubscribeMessageLatencyMicroseconds, "endpoint", t).Update(te.Seconds())
|
|
||||||
m.opts.Meter.Histogram(SubscribeMessageDurationSeconds, "endpoint", t).Update(te.Seconds())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) {
|
func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
|
m.RLock()
|
||||||
|
if !m.connected {
|
||||||
|
m.RUnlock()
|
||||||
|
return nil, ErrNotConnected
|
||||||
|
}
|
||||||
|
m.RUnlock()
|
||||||
|
|
||||||
|
sid, err := id.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := NewSubscribeOptions(opts...)
|
||||||
|
|
||||||
|
sub := &memorySubscriber{
|
||||||
|
exit: make(chan bool, 1),
|
||||||
|
id: sid,
|
||||||
|
topic: topic,
|
||||||
|
batchhandler: handler,
|
||||||
|
opts: options,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
m.subscribers[topic] = append(m.subscribers[topic], sub)
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sub.exit
|
||||||
|
m.Lock()
|
||||||
|
newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
|
||||||
|
for _, sb := range m.subscribers[topic] {
|
||||||
|
if sb.id == sub.id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSubscribers = append(newSubscribers, sb)
|
||||||
|
}
|
||||||
|
m.subscribers[topic] = newSubscribers
|
||||||
|
m.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return sub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
if !m.connected {
|
if !m.connected {
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
@@ -232,41 +286,38 @@ func (m *memoryBroker) Name() string {
|
|||||||
return m.opts.Name
|
return m.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
type memoryMessage struct {
|
func (m *memoryEvent) Topic() string {
|
||||||
err error
|
|
||||||
body interface{}
|
|
||||||
topic string
|
|
||||||
opts PublishOptions
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memoryMessage) Topic() string {
|
|
||||||
return m.topic
|
return m.topic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryMessage) Body() interface{} {
|
func (m *memoryEvent) Message() *Message {
|
||||||
return m.body
|
switch v := m.message.(type) {
|
||||||
}
|
case *Message:
|
||||||
|
return v
|
||||||
|
case []byte:
|
||||||
|
msg := &Message{}
|
||||||
|
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
|
||||||
|
if m.opts.Logger.V(logger.ErrorLevel) {
|
||||||
|
m.opts.Logger.Error(m.opts.Context, "[memory]: failed to unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
func (m *memoryMessage) Ack() error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryMessage) Error() error {
|
func (m *memoryEvent) Ack() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryEvent) Error() error {
|
||||||
return m.err
|
return m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memoryMessage) Context() context.Context {
|
func (m *memoryEvent) SetError(err error) {
|
||||||
return m.ctx
|
m.err = err
|
||||||
}
|
|
||||||
|
|
||||||
type memorySubscriber struct {
|
|
||||||
ctx context.Context
|
|
||||||
exit chan bool
|
|
||||||
handler interface{}
|
|
||||||
id string
|
|
||||||
topic string
|
|
||||||
opts SubscribeOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *memorySubscriber) Options() SubscribeOptions {
|
func (m *memorySubscriber) Options() SubscribeOptions {
|
||||||
@@ -283,7 +334,7 @@ func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewBroker return new memory broker
|
// NewBroker return new memory broker
|
||||||
func NewBroker(opts ...Option) *memoryBroker {
|
func NewBroker(opts ...Option) Broker {
|
||||||
return &memoryBroker{
|
return &memoryBroker{
|
||||||
opts: NewOptions(opts...),
|
opts: NewOptions(opts...),
|
||||||
subscribers: make(map[string][]*memorySubscriber),
|
subscribers: make(map[string][]*memorySubscriber),
|
||||||
|
@@ -7,39 +7,19 @@ import (
|
|||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"go.unistack.org/micro/v4/meter"
|
"go.unistack.org/micro/v4/meter"
|
||||||
"go.unistack.org/micro/v4/register"
|
"go.unistack.org/micro/v4/register"
|
||||||
"go.unistack.org/micro/v4/tracer"
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// PublishMessageDurationSeconds specifies meter metric name
|
|
||||||
PublishMessageDurationSeconds = "publish_message_duration_seconds"
|
|
||||||
// PublishMessageLatencyMicroseconds specifies meter metric name
|
|
||||||
PublishMessageLatencyMicroseconds = "publish_message_latency_microseconds"
|
|
||||||
// PublishMessageTotal specifies meter metric name
|
|
||||||
PublishMessageTotal = "publish_message_total"
|
|
||||||
// PublishMessageInflight specifies meter metric name
|
|
||||||
PublishMessageInflight = "publish_message_inflight"
|
|
||||||
// SubscribeMessageDurationSeconds specifies meter metric name
|
|
||||||
SubscribeMessageDurationSeconds = "subscribe_message_duration_seconds"
|
|
||||||
// SubscribeMessageLatencyMicroseconds specifies meter metric name
|
|
||||||
SubscribeMessageLatencyMicroseconds = "subscribe_message_latency_microseconds"
|
|
||||||
// SubscribeMessageTotal specifies meter metric name
|
|
||||||
SubscribeMessageTotal = "subscribe_message_total"
|
|
||||||
// SubscribeMessageInflight specifies meter metric name
|
|
||||||
SubscribeMessageInflight = "subscribe_message_inflight"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options struct
|
// Options struct
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Tracer used for tracing
|
// Tracer used for tracing
|
||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
// Register can be used for clustering
|
// Register can be used for clustering
|
||||||
Register register.Register
|
Register register.Register
|
||||||
// Codecs holds the codec for marshal/unmarshal
|
// Codec holds the codec for marshal/unmarshal
|
||||||
Codecs map[string]codec.Codec
|
Codec codec.Codec
|
||||||
// Logger used for logging
|
// Logger used for logging
|
||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
// Meter used for metrics
|
// Meter used for metrics
|
||||||
@@ -49,16 +29,15 @@ type Options struct {
|
|||||||
// TLSConfig holds tls.TLSConfig options
|
// TLSConfig holds tls.TLSConfig options
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
// ErrorHandler used when broker can't unmarshal incoming message
|
// ErrorHandler used when broker can't unmarshal incoming message
|
||||||
ErrorHandler func(Message)
|
ErrorHandler Handler
|
||||||
|
// BatchErrorHandler used when broker can't unmashal incoming messages
|
||||||
|
BatchErrorHandler BatchHandler
|
||||||
// Name holds the broker name
|
// Name holds the broker name
|
||||||
Name string
|
Name string
|
||||||
// Addrs holds the broker address
|
// Addrs holds the broker address
|
||||||
Addrs []string
|
Addrs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option func
|
|
||||||
type Option func(*Options)
|
|
||||||
|
|
||||||
// NewOptions create new Options
|
// NewOptions create new Options
|
||||||
func NewOptions(opts ...Option) Options {
|
func NewOptions(opts ...Option) Options {
|
||||||
options := Options{
|
options := Options{
|
||||||
@@ -66,7 +45,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Codecs: make(map[string]codec.Codec),
|
Codec: codec.DefaultCodec,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
@@ -82,32 +61,6 @@ func Context(ctx context.Context) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageOption func
|
|
||||||
type MessageOption func(*MessageOptions)
|
|
||||||
|
|
||||||
// MessageOptions struct
|
|
||||||
type MessageOptions struct {
|
|
||||||
Metadata metadata.Metadata
|
|
||||||
ContentType string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageMetadata pass additional message metadata
|
|
||||||
func MessageMetadata(md metadata.Metadata) MessageOption {
|
|
||||||
return func(o *MessageOptions) {
|
|
||||||
o.Metadata = md
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageContentType pass ContentType for message data
|
|
||||||
func MessageContentType(ct string) MessageOption {
|
|
||||||
return func(o *MessageOptions) {
|
|
||||||
o.ContentType = ct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishOption func
|
|
||||||
type PublishOption func(*PublishOptions)
|
|
||||||
|
|
||||||
// PublishOptions struct
|
// PublishOptions struct
|
||||||
type PublishOptions struct {
|
type PublishOptions struct {
|
||||||
// Context holds external options
|
// Context holds external options
|
||||||
@@ -132,9 +85,11 @@ type SubscribeOptions struct {
|
|||||||
// Context holds external options
|
// Context holds external options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// ErrorHandler used when broker can't unmarshal incoming message
|
// ErrorHandler used when broker can't unmarshal incoming message
|
||||||
ErrorHandler func(Message)
|
ErrorHandler Handler
|
||||||
// QueueGroup holds consumer group
|
// BatchErrorHandler used when broker can't unmashal incoming messages
|
||||||
QueueGroup string
|
BatchErrorHandler BatchHandler
|
||||||
|
// Group holds consumer group
|
||||||
|
Group string
|
||||||
// AutoAck flag specifies auto ack of incoming message when no error happens
|
// AutoAck flag specifies auto ack of incoming message when no error happens
|
||||||
AutoAck bool
|
AutoAck bool
|
||||||
// BodyOnly flag specifies that message contains only body bytes without header
|
// BodyOnly flag specifies that message contains only body bytes without header
|
||||||
@@ -145,6 +100,12 @@ type SubscribeOptions struct {
|
|||||||
BatchWait time.Duration
|
BatchWait time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option func
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// PublishOption func
|
||||||
|
type PublishOption func(*PublishOptions)
|
||||||
|
|
||||||
// PublishBodyOnly publish only body of the message
|
// PublishBodyOnly publish only body of the message
|
||||||
func PublishBodyOnly(b bool) PublishOption {
|
func PublishBodyOnly(b bool) PublishOption {
|
||||||
return func(o *PublishOptions) {
|
return func(o *PublishOptions) {
|
||||||
@@ -168,29 +129,59 @@ func Addrs(addrs ...string) Option {
|
|||||||
|
|
||||||
// Codec sets the codec used for encoding/decoding used where
|
// Codec sets the codec used for encoding/decoding used where
|
||||||
// a broker does not support headers
|
// a broker does not support headers
|
||||||
// Codec to be used to encode/decode requests for a given content type
|
func Codec(c codec.Codec) Option {
|
||||||
func Codec(contentType string, c codec.Codec) Option {
|
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.Codecs[contentType] = c
|
o.Codec = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorHandler will catch all broker errors that cant be handled
|
// ErrorHandler will catch all broker errors that cant be handled
|
||||||
// in normal way, for example Codec errors
|
// in normal way, for example Codec errors
|
||||||
func ErrorHandler(h func(Message)) Option {
|
func ErrorHandler(h Handler) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
o.ErrorHandler = h
|
o.ErrorHandler = h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func BatchErrorHandler(h BatchHandler) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.BatchErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeErrorHandler will catch all broker errors that cant be handled
|
// SubscribeErrorHandler will catch all broker errors that cant be handled
|
||||||
// in normal way, for example Codec errors
|
// in normal way, for example Codec errors
|
||||||
func SubscribeErrorHandler(h func(Message)) SubscribeOption {
|
func SubscribeErrorHandler(h Handler) SubscribeOption {
|
||||||
return func(o *SubscribeOptions) {
|
return func(o *SubscribeOptions) {
|
||||||
o.ErrorHandler = h
|
o.ErrorHandler = h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscribeBatchErrorHandler will catch all broker errors that cant be handled
|
||||||
|
// in normal way, for example Codec errors
|
||||||
|
func SubscribeBatchErrorHandler(h BatchHandler) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.BatchErrorHandler = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue sets the subscribers queue
|
||||||
|
// Deprecated
|
||||||
|
func Queue(name string) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.Group = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeGroup sets the name of the queue to share messages on
|
||||||
|
func SubscribeGroup(name string) SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.Group = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register sets register option
|
// Register sets register option
|
||||||
func Register(r register.Register) Option {
|
func Register(r register.Register) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -233,21 +224,6 @@ func Name(n string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscribeOption func signature
|
|
||||||
type SubscribeOption func(*SubscribeOptions)
|
|
||||||
|
|
||||||
// NewSubscribeOptions creates new SubscribeOptions
|
|
||||||
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
|
|
||||||
options := SubscribeOptions{
|
|
||||||
AutoAck: true,
|
|
||||||
Context: context.Background(),
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
o(&options)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeContext set context
|
// SubscribeContext set context
|
||||||
func SubscribeContext(ctx context.Context) SubscribeOption {
|
func SubscribeContext(ctx context.Context) SubscribeOption {
|
||||||
return func(o *SubscribeOptions) {
|
return func(o *SubscribeOptions) {
|
||||||
@@ -255,6 +231,14 @@ func SubscribeContext(ctx context.Context) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableAutoAck disables auto ack
|
||||||
|
// Deprecated
|
||||||
|
func DisableAutoAck() SubscribeOption {
|
||||||
|
return func(o *SubscribeOptions) {
|
||||||
|
o.AutoAck = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SubscribeAutoAck contol auto acking of messages
|
// SubscribeAutoAck contol auto acking of messages
|
||||||
// after they have been handled.
|
// after they have been handled.
|
||||||
func SubscribeAutoAck(b bool) SubscribeOption {
|
func SubscribeAutoAck(b bool) SubscribeOption {
|
||||||
@@ -284,16 +268,17 @@ func SubscribeBatchWait(td time.Duration) SubscribeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscribeQueueGroup sets the shared queue name distributed messages across subscribers
|
// SubscribeOption func
|
||||||
func SubscribeQueueGroup(n string) SubscribeOption {
|
type SubscribeOption func(*SubscribeOptions)
|
||||||
return func(o *SubscribeOptions) {
|
|
||||||
o.QueueGroup = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeAutoAck control auto ack processing for handler
|
// NewSubscribeOptions creates new SubscribeOptions
|
||||||
func SubscribeAuthAck(b bool) SubscribeOption {
|
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
|
||||||
return func(o *SubscribeOptions) {
|
options := SubscribeOptions{
|
||||||
o.AutoAck = b
|
AutoAck: true,
|
||||||
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
@@ -1,98 +0,0 @@
|
|||||||
package broker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
subSig = "func(context.Context, interface{}) error"
|
|
||||||
batchSubSig = "func([]context.Context, []interface{}) error"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Precompute the reflect type for error. Can't use error directly
|
|
||||||
// because Typeof takes an empty interface value. This is annoying.
|
|
||||||
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
|
||||||
|
|
||||||
// Is this an exported - upper case - name?
|
|
||||||
func isExported(name string) bool {
|
|
||||||
rune, _ := utf8.DecodeRuneInString(name)
|
|
||||||
return unicode.IsUpper(rune)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is this type exported or a builtin?
|
|
||||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
|
||||||
for t.Kind() == reflect.Ptr {
|
|
||||||
t = t.Elem()
|
|
||||||
}
|
|
||||||
// PkgPath will be non-empty even for an exported type,
|
|
||||||
// so we need to check the type name as well.
|
|
||||||
return isExported(t.Name()) || t.PkgPath() == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateSubscriber func signature
|
|
||||||
func ValidateSubscriber(sub interface{}) error {
|
|
||||||
typ := reflect.TypeOf(sub)
|
|
||||||
var argType reflect.Type
|
|
||||||
switch typ.Kind() {
|
|
||||||
case reflect.Func:
|
|
||||||
name := "Func"
|
|
||||||
switch typ.NumIn() {
|
|
||||||
case 1: // func(Message) error
|
|
||||||
|
|
||||||
case 2: // func(context.Context, Message) error or func(context.Context, []Message) error
|
|
||||||
argType = typ.In(2)
|
|
||||||
// if sub.Options().Batch {
|
|
||||||
if argType.Kind() != reflect.Slice {
|
|
||||||
return fmt.Errorf("subscriber %v dont have required signature %s", name, batchSubSig)
|
|
||||||
}
|
|
||||||
if strings.Compare(fmt.Sprintf("%v", argType), "[]interface{}") == 0 {
|
|
||||||
return fmt.Errorf("subscriber %v dont have required signaure %s", name, batchSubSig)
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s or %s", name, typ.NumIn(), subSig, batchSubSig)
|
|
||||||
}
|
|
||||||
if !isExportedOrBuiltinType(argType) {
|
|
||||||
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
|
|
||||||
}
|
|
||||||
if typ.NumOut() != 1 {
|
|
||||||
return fmt.Errorf("subscriber %v has wrong number of return values: %v require signature %s or %s",
|
|
||||||
name, typ.NumOut(), subSig, batchSubSig)
|
|
||||||
}
|
|
||||||
if returnType := typ.Out(0); returnType != typeOfError {
|
|
||||||
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
hdlr := reflect.ValueOf(sub)
|
|
||||||
name := reflect.Indirect(hdlr).Type().Name()
|
|
||||||
|
|
||||||
for m := 0; m < typ.NumMethod(); m++ {
|
|
||||||
method := typ.Method(m)
|
|
||||||
switch method.Type.NumIn() {
|
|
||||||
case 3:
|
|
||||||
argType = method.Type.In(2)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s or %s",
|
|
||||||
name, method.Name, method.Type.NumIn(), subSig, batchSubSig)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isExportedOrBuiltinType(argType) {
|
|
||||||
return fmt.Errorf("%v argument type not exported: %v", name, argType)
|
|
||||||
}
|
|
||||||
if method.Type.NumOut() != 1 {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"subscriber %v.%v has wrong number of return values: %v require signature %s or %s",
|
|
||||||
name, method.Name, method.Type.NumOut(), subSig, batchSubSig)
|
|
||||||
}
|
|
||||||
if returnType := method.Type.Out(0); returnType != typeOfError {
|
|
||||||
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -34,12 +35,23 @@ type Client interface {
|
|||||||
Name() string
|
Name() string
|
||||||
Init(opts ...Option) error
|
Init(opts ...Option) error
|
||||||
Options() Options
|
Options() Options
|
||||||
|
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
|
||||||
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
|
NewRequest(service string, endpoint string, req interface{}, opts ...RequestOption) Request
|
||||||
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
|
||||||
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
|
||||||
|
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
|
||||||
|
BatchPublish(ctx context.Context, msg []Message, opts ...PublishOption) error
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Message is the interface for publishing asynchronously
|
||||||
|
type Message interface {
|
||||||
|
Topic() string
|
||||||
|
Payload() interface{}
|
||||||
|
ContentType() string
|
||||||
|
Metadata() metadata.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
// Request is the interface for a synchronous request used by Call or Stream
|
// Request is the interface for a synchronous request used by Call or Stream
|
||||||
type Request interface {
|
type Request interface {
|
||||||
// The service to call
|
// The service to call
|
||||||
@@ -56,22 +68,16 @@ type Request interface {
|
|||||||
Codec() codec.Codec
|
Codec() codec.Codec
|
||||||
// indicates whether the request will be a streaming one rather than unary
|
// indicates whether the request will be a streaming one rather than unary
|
||||||
Stream() bool
|
Stream() bool
|
||||||
// Header data
|
|
||||||
// Header() metadata.Metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response is the response received from a service
|
// Response is the response received from a service
|
||||||
type Response interface {
|
type Response interface {
|
||||||
// Read the response
|
// Read the response
|
||||||
Codec() codec.Codec
|
Codec() codec.Codec
|
||||||
// The content type
|
|
||||||
// ContentType() string
|
|
||||||
// Header data
|
// Header data
|
||||||
// Header() metadata.Metadata
|
Header() metadata.Metadata
|
||||||
// Read the undecoded response
|
// Read the undecoded response
|
||||||
Read() ([]byte, error)
|
Read() ([]byte, error)
|
||||||
// The unencoded request body
|
|
||||||
// Body() interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream is the interface for a bidirectional synchronous stream
|
// Stream is the interface for a bidirectional synchronous stream
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v4/errors"
|
"go.unistack.org/micro/v4/errors"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
@@ -284,9 +285,6 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
ch := make(chan error, callOpts.Retries)
|
ch := make(chan error, callOpts.Retries)
|
||||||
var gerr error
|
var gerr error
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
|
||||||
n.opts.Meter.Counter(ClientRequestInflight, "endpoint", endpoint).Inc()
|
|
||||||
for i := 0; i <= callOpts.Retries; i++ {
|
for i := 0; i <= callOpts.Retries; i++ {
|
||||||
go func() {
|
go func() {
|
||||||
ch <- call(i)
|
ch <- call(i)
|
||||||
@@ -314,16 +312,6 @@ func (n *noopClient) Call(ctx context.Context, req Request, rsp interface{}, opt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gerr != nil {
|
|
||||||
n.opts.Meter.Counter(ClientRequestTotal, "endpoint", endpoint, "status", "failure").Inc()
|
|
||||||
} else {
|
|
||||||
n.opts.Meter.Counter(ClientRequestTotal, "endpoint", endpoint, "status", "success").Inc()
|
|
||||||
}
|
|
||||||
n.opts.Meter.Counter(ClientRequestInflight, "endpoint", endpoint).Dec()
|
|
||||||
te := time.Since(ts)
|
|
||||||
n.opts.Meter.Summary(ClientRequestLatencyMicroseconds, "endpoint", endpoint).Update(te.Seconds())
|
|
||||||
n.opts.Meter.Histogram(ClientRequestDurationSeconds, "endpoint", endpoint).Update(te.Seconds())
|
|
||||||
|
|
||||||
return gerr
|
return gerr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +323,11 @@ func (n *noopClient) NewRequest(service, endpoint string, req interface{}, opts
|
|||||||
return &noopRequest{service: service, endpoint: endpoint}
|
return &noopRequest{service: service, endpoint: endpoint}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) NewMessage(topic string, msg interface{}, opts ...MessageOption) Message {
|
||||||
|
options := NewMessageOptions(append([]MessageOption{MessageContentType(n.opts.ContentType)}, opts...)...)
|
||||||
|
return &noopMessage{topic: topic, payload: msg, opts: options}
|
||||||
|
}
|
||||||
|
|
||||||
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -421,15 +414,7 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
|
|
||||||
node := next()
|
node := next()
|
||||||
|
|
||||||
// ts := time.Now()
|
|
||||||
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
|
||||||
n.opts.Meter.Counter(ClientRequestInflight, "endpoint", endpoint).Inc()
|
|
||||||
stream, cerr := n.stream(ctx, node, req, callOpts)
|
stream, cerr := n.stream(ctx, node, req, callOpts)
|
||||||
if cerr != nil {
|
|
||||||
n.opts.Meter.Counter(ClientRequestTotal, "endpoint", endpoint, "status", "failure").Inc()
|
|
||||||
} else {
|
|
||||||
n.opts.Meter.Counter(ClientRequestTotal, "endpoint", endpoint, "status", "success").Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
// record the result of the call to inform future routing decisions
|
// record the result of the call to inform future routing decisions
|
||||||
if verr := n.opts.Selector.Record(node, cerr); verr != nil {
|
if verr := n.opts.Selector.Record(node, cerr); verr != nil {
|
||||||
@@ -483,6 +468,64 @@ func (n *noopClient) Stream(ctx context.Context, req Request, opts ...CallOption
|
|||||||
return nil, grr
|
return nil, grr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noopClient) stream(ctx context.Context, addr string, req Request, opts CallOptions) (*noopStream, error) {
|
func (n *noopClient) stream(ctx context.Context, addr string, req Request, opts CallOptions) (Stream, error) {
|
||||||
return &noopStream{}, nil
|
return &noopStream{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) BatchPublish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
||||||
|
return n.publish(ctx, ps, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) Publish(ctx context.Context, p Message, opts ...PublishOption) error {
|
||||||
|
return n.publish(ctx, []Message{p}, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopClient) publish(ctx context.Context, ps []Message, opts ...PublishOption) error {
|
||||||
|
options := NewPublishOptions(opts...)
|
||||||
|
|
||||||
|
msgs := make([]*broker.Message, 0, len(ps))
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
md, ok := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
md = metadata.New(0)
|
||||||
|
}
|
||||||
|
md[metadata.HeaderContentType] = p.ContentType()
|
||||||
|
|
||||||
|
topic := p.Topic()
|
||||||
|
|
||||||
|
// get the exchange
|
||||||
|
if len(options.Exchange) > 0 {
|
||||||
|
topic = options.Exchange
|
||||||
|
}
|
||||||
|
|
||||||
|
md[metadata.HeaderTopic] = topic
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
// passed in raw data
|
||||||
|
if d, ok := p.Payload().(*codec.Frame); ok {
|
||||||
|
body = d.Data
|
||||||
|
} else {
|
||||||
|
// use codec for payload
|
||||||
|
cf, err := n.newCodec(p.ContentType())
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the body
|
||||||
|
b, err := cf.Marshal(p.Payload())
|
||||||
|
if err != nil {
|
||||||
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
|
}
|
||||||
|
body = b
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs = append(msgs, &broker.Message{Header: md, Body: body})
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.opts.Broker.BatchPublish(ctx, msgs,
|
||||||
|
broker.PublishContext(options.Context),
|
||||||
|
broker.PublishBodyOnly(options.BodyOnly),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
@@ -18,17 +19,6 @@ import (
|
|||||||
"go.unistack.org/micro/v4/tracer"
|
"go.unistack.org/micro/v4/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// ClientRequestDurationSeconds specifies meter metric name
|
|
||||||
ClientRequestDurationSeconds = "client_request_duration_seconds"
|
|
||||||
// ClientRequestLatencyMicroseconds specifies meter metric name
|
|
||||||
ClientRequestLatencyMicroseconds = "client_request_latency_microseconds"
|
|
||||||
// ClientRequestTotal specifies meter metric name
|
|
||||||
ClientRequestTotal = "client_request_total"
|
|
||||||
// ClientRequestInflight specifies meter metric name
|
|
||||||
ClientRequestInflight = "client_request_inflight"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options holds client options
|
// Options holds client options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// Transport used for transfer messages
|
// Transport used for transfer messages
|
||||||
@@ -39,6 +29,8 @@ type Options struct {
|
|||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
// Tracer used for tracing
|
// Tracer used for tracing
|
||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
|
// Broker used to publish messages
|
||||||
|
Broker broker.Broker
|
||||||
// Meter used for metrics
|
// Meter used for metrics
|
||||||
Meter meter.Meter
|
Meter meter.Meter
|
||||||
// Context is used for external options
|
// Context is used for external options
|
||||||
@@ -207,6 +199,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
PoolTTL: DefaultPoolTTL,
|
PoolTTL: DefaultPoolTTL,
|
||||||
Selector: random.NewSelector(),
|
Selector: random.NewSelector(),
|
||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
|
Broker: broker.DefaultBroker,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
Router: router.DefaultRouter,
|
Router: router.DefaultRouter,
|
||||||
@@ -220,6 +213,13 @@ func NewOptions(opts ...Option) Options {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broker to be used for pub/sub
|
||||||
|
func Broker(b broker.Broker) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Broker = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tracer to be used for tracing
|
// Tracer to be used for tracing
|
||||||
func Tracer(t tracer.Tracer) Option {
|
func Tracer(t tracer.Tracer) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
|
@@ -14,10 +14,9 @@ type cfg struct {
|
|||||||
StringValue string `default:"string_value"`
|
StringValue string `default:"string_value"`
|
||||||
IgnoreValue string `json:"-"`
|
IgnoreValue string `json:"-"`
|
||||||
StructValue *cfgStructValue
|
StructValue *cfgStructValue
|
||||||
IntValue int `default:"99"`
|
IntValue int `default:"99"`
|
||||||
DurationValue time.Duration `default:"10s"`
|
DurationValue time.Duration `default:"10s"`
|
||||||
MDurationValue mtime.Duration `default:"10s"`
|
MDurationValue mtime.Duration `default:"10s"`
|
||||||
MapValue map[string]bool `default:"key1=true,key2=false"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type cfgStructValue struct {
|
type cfgStructValue struct {
|
||||||
@@ -68,9 +67,6 @@ func TestDefault(t *testing.T) {
|
|||||||
if conf.StringValue != "after_load" {
|
if conf.StringValue != "after_load" {
|
||||||
t.Fatal("AfterLoad option not working")
|
t.Fatal("AfterLoad option not working")
|
||||||
}
|
}
|
||||||
if len(conf.MapValue) != 2 {
|
|
||||||
t.Fatalf("map value invalid: %#+v\n", conf.MapValue)
|
|
||||||
}
|
|
||||||
_ = conf
|
_ = conf
|
||||||
// t.Logf("%#+v\n", conf)
|
// t.Logf("%#+v\n", conf)
|
||||||
}
|
}
|
||||||
|
27
event.go
Normal file
27
event.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package micro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event is used to publish messages to a topic
|
||||||
|
type Event interface {
|
||||||
|
// Publish publishes a message to the event topic
|
||||||
|
Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type event struct {
|
||||||
|
c client.Client
|
||||||
|
topic string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEvent creates a new event publisher
|
||||||
|
func NewEvent(topic string, c client.Client) Event {
|
||||||
|
return &event{c, topic}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *event) Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error {
|
||||||
|
return e.c.Publish(ctx, e.c.NewMessage(e.topic, msg), opts...)
|
||||||
|
}
|
13
go.mod
13
go.mod
@@ -4,16 +4,7 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||||
github.com/imdario/mergo v0.3.15
|
github.com/imdario/mergo v0.3.14
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5
|
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35
|
||||||
golang.org/x/sync v0.1.0
|
|
||||||
golang.org/x/sys v0.7.0
|
|
||||||
google.golang.org/grpc v1.54.0
|
|
||||||
google.golang.org/protobuf v1.30.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
|
||||||
)
|
)
|
||||||
|
28
go.sum
28
go.sum
@@ -1,31 +1,11 @@
|
|||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
|
||||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
|
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35 h1:4mohWoM/UGg1BvFFiqSPRl5uwJY3rVV0HQX0ETqauqQ=
|
||||||
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
github.com/silas/dag v0.0.0-20211117232152-9d50aa809f35/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
|
||||||
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
|
|
||||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@@ -39,6 +39,8 @@ func FromOutgoingContext(ctx context.Context) (Metadata, bool) {
|
|||||||
|
|
||||||
// FromContext returns metadata from the given context
|
// FromContext returns metadata from the given context
|
||||||
// returned metadata shoud not be modified or race condition happens
|
// returned metadata shoud not be modified or race condition happens
|
||||||
|
//
|
||||||
|
// Deprecated: use FromIncomingContext or FromOutgoingContext
|
||||||
func FromContext(ctx context.Context) (Metadata, bool) {
|
func FromContext(ctx context.Context) (Metadata, bool) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
@@ -51,6 +53,8 @@ func FromContext(ctx context.Context) (Metadata, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewContext creates a new context with the given metadata
|
// NewContext creates a new context with the given metadata
|
||||||
|
//
|
||||||
|
// Deprecated: use NewIncomingContext or NewOutgoingContext
|
||||||
func NewContext(ctx context.Context, md Metadata) context.Context {
|
func NewContext(ctx context.Context, md Metadata) context.Context {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@@ -11,6 +11,39 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// ClientRequestDurationSeconds specifies meter metric name
|
||||||
|
ClientRequestDurationSeconds = "client_request_duration_seconds"
|
||||||
|
// ClientRequestLatencyMicroseconds specifies meter metric name
|
||||||
|
ClientRequestLatencyMicroseconds = "client_request_latency_microseconds"
|
||||||
|
// ClientRequestTotal specifies meter metric name
|
||||||
|
ClientRequestTotal = "client_request_total"
|
||||||
|
// ClientRequestInflight specifies meter metric name
|
||||||
|
ClientRequestInflight = "client_request_inflight"
|
||||||
|
// ServerRequestDurationSeconds specifies meter metric name
|
||||||
|
ServerRequestDurationSeconds = "server_request_duration_seconds"
|
||||||
|
// ServerRequestLatencyMicroseconds specifies meter metric name
|
||||||
|
ServerRequestLatencyMicroseconds = "server_request_latency_microseconds"
|
||||||
|
// ServerRequestTotal specifies meter metric name
|
||||||
|
ServerRequestTotal = "server_request_total"
|
||||||
|
// ServerRequestInflight specifies meter metric name
|
||||||
|
ServerRequestInflight = "server_request_inflight"
|
||||||
|
// PublishMessageDurationSeconds specifies meter metric name
|
||||||
|
PublishMessageDurationSeconds = "publish_message_duration_seconds"
|
||||||
|
// PublishMessageLatencyMicroseconds specifies meter metric name
|
||||||
|
PublishMessageLatencyMicroseconds = "publish_message_latency_microseconds"
|
||||||
|
// PublishMessageTotal specifies meter metric name
|
||||||
|
PublishMessageTotal = "publish_message_total"
|
||||||
|
// PublishMessageInflight specifies meter metric name
|
||||||
|
PublishMessageInflight = "publish_message_inflight"
|
||||||
|
// SubscribeMessageDurationSeconds specifies meter metric name
|
||||||
|
SubscribeMessageDurationSeconds = "subscribe_message_duration_seconds"
|
||||||
|
// SubscribeMessageLatencyMicroseconds specifies meter metric name
|
||||||
|
SubscribeMessageLatencyMicroseconds = "subscribe_message_latency_microseconds"
|
||||||
|
// SubscribeMessageTotal specifies meter metric name
|
||||||
|
SubscribeMessageTotal = "subscribe_message_total"
|
||||||
|
// SubscribeMessageInflight specifies meter metric name
|
||||||
|
SubscribeMessageInflight = "subscribe_message_inflight"
|
||||||
|
|
||||||
labelSuccess = "success"
|
labelSuccess = "success"
|
||||||
labelFailure = "failure"
|
labelFailure = "failure"
|
||||||
labelStatus = "status"
|
labelStatus = "status"
|
||||||
@@ -197,7 +230,37 @@ func (w *wrapper) Stream(ctx context.Context, req client.Request, opts ...client
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *wrapper) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
|
func (w *wrapper) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
|
||||||
return w.Client.Publish(ctx, p, opts...)
|
endpoint := p.Topic()
|
||||||
|
|
||||||
|
labels := make([]string, 0, 4)
|
||||||
|
labels = append(labels, labelEndpoint, endpoint)
|
||||||
|
|
||||||
|
w.opts.Meter.Counter(PublishMessageInflight, labels...).Inc()
|
||||||
|
ts := time.Now()
|
||||||
|
err := w.Client.Publish(ctx, p, opts...)
|
||||||
|
te := time.Since(ts)
|
||||||
|
w.opts.Meter.Counter(PublishMessageInflight, labels...).Dec()
|
||||||
|
|
||||||
|
w.opts.Meter.Summary(PublishMessageLatencyMicroseconds, labels...).Update(te.Seconds())
|
||||||
|
w.opts.Meter.Histogram(PublishMessageDurationSeconds, labels...).Update(te.Seconds())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
labels = append(labels, labelStatus, labelSuccess)
|
||||||
|
} else {
|
||||||
|
labels = append(labels, labelStatus, labelFailure)
|
||||||
|
}
|
||||||
|
w.opts.Meter.Counter(PublishMessageTotal, labels...).Inc()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandlerWrapper create new server handler wrapper
|
||||||
|
// deprecated
|
||||||
|
func NewHandlerWrapper(opts ...Option) server.HandlerWrapper {
|
||||||
|
handler := &wrapper{
|
||||||
|
opts: NewOptions(opts...),
|
||||||
|
}
|
||||||
|
return handler.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServerHandlerWrapper create new server handler wrapper
|
// NewServerHandlerWrapper create new server handler wrapper
|
||||||
@@ -239,3 +302,46 @@ func (w *wrapper) HandlerFunc(fn server.HandlerFunc) server.HandlerFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSubscriberWrapper create server subscribe wrapper
|
||||||
|
// deprecated
|
||||||
|
func NewSubscriberWrapper(opts ...Option) server.SubscriberWrapper {
|
||||||
|
handler := &wrapper{
|
||||||
|
opts: NewOptions(opts...),
|
||||||
|
}
|
||||||
|
return handler.SubscriberFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerSubscriberWrapper(opts ...Option) server.SubscriberWrapper {
|
||||||
|
handler := &wrapper{
|
||||||
|
opts: NewOptions(opts...),
|
||||||
|
}
|
||||||
|
return handler.SubscriberFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) SubscriberFunc(fn server.SubscriberFunc) server.SubscriberFunc {
|
||||||
|
return func(ctx context.Context, msg server.Message) error {
|
||||||
|
endpoint := msg.Topic()
|
||||||
|
|
||||||
|
labels := make([]string, 0, 4)
|
||||||
|
labels = append(labels, labelEndpoint, endpoint)
|
||||||
|
|
||||||
|
w.opts.Meter.Counter(SubscribeMessageInflight, labels...).Inc()
|
||||||
|
ts := time.Now()
|
||||||
|
err := fn(ctx, msg)
|
||||||
|
te := time.Since(ts)
|
||||||
|
w.opts.Meter.Counter(SubscribeMessageInflight, labels...).Dec()
|
||||||
|
|
||||||
|
w.opts.Meter.Summary(SubscribeMessageLatencyMicroseconds, labels...).Update(te.Seconds())
|
||||||
|
w.opts.Meter.Histogram(SubscribeMessageDurationSeconds, labels...).Update(te.Seconds())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
labels = append(labels, labelStatus, labelSuccess)
|
||||||
|
} else {
|
||||||
|
labels = append(labels, labelStatus, labelFailure)
|
||||||
|
}
|
||||||
|
w.opts.Meter.Counter(SubscribeMessageTotal, labels...).Inc()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
28
options.go
28
options.go
@@ -90,7 +90,33 @@ type Option func(*Options) error
|
|||||||
// Broker to be used for client and server
|
// Broker to be used for client and server
|
||||||
func Broker(b broker.Broker, opts ...BrokerOption) Option {
|
func Broker(b broker.Broker, opts ...BrokerOption) Option {
|
||||||
return func(o *Options) error {
|
return func(o *Options) error {
|
||||||
o.Brokers = []broker.Broker{b}
|
var err error
|
||||||
|
bopts := brokerOptions{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&bopts)
|
||||||
|
}
|
||||||
|
all := false
|
||||||
|
if len(opts) == 0 {
|
||||||
|
all = true
|
||||||
|
}
|
||||||
|
for _, srv := range o.Servers {
|
||||||
|
for _, os := range bopts.servers {
|
||||||
|
if srv.Name() == os || all {
|
||||||
|
if err = srv.Init(server.Broker(b)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, cli := range o.Clients {
|
||||||
|
for _, oc := range bopts.clients {
|
||||||
|
if cli.Name() == oc || all {
|
||||||
|
if err = cli.Init(client.Broker(b)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,6 +33,16 @@ func SetOption(k, v interface{}) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSubscriberOption returns a function to setup a context with given value
|
||||||
|
func SetSubscriberOption(k, v interface{}) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
if o.Context == nil {
|
||||||
|
o.Context = context.Background()
|
||||||
|
}
|
||||||
|
o.Context = context.WithValue(o.Context, k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetHandlerOption returns a function to setup a context with given value
|
// SetHandlerOption returns a function to setup a context with given value
|
||||||
func SetHandlerOption(k, v interface{}) HandlerOption {
|
func SetHandlerOption(k, v interface{}) HandlerOption {
|
||||||
return func(o *HandlerOptions) {
|
return func(o *HandlerOptions) {
|
||||||
|
@@ -51,3 +51,14 @@ func TestSetOption(t *testing.T) {
|
|||||||
t.Fatal("SetOption not works")
|
t.Fatal("SetOption not works")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetSubscriberOption(t *testing.T) {
|
||||||
|
type key struct{}
|
||||||
|
o := SetSubscriberOption(key{}, "test")
|
||||||
|
opts := &SubscriberOptions{}
|
||||||
|
o(opts)
|
||||||
|
|
||||||
|
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
|
||||||
|
t.Fatal("SetSubscriberOption not works")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
59
server/handler.go
Normal file
59
server/handler.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/register"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rpcHandler struct {
|
||||||
|
opts HandlerOptions
|
||||||
|
handler interface{}
|
||||||
|
name string
|
||||||
|
endpoints []*register.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRPCHandler(handler interface{}, opts ...HandlerOption) Handler {
|
||||||
|
options := NewHandlerOptions(opts...)
|
||||||
|
|
||||||
|
typ := reflect.TypeOf(handler)
|
||||||
|
hdlr := reflect.ValueOf(handler)
|
||||||
|
name := reflect.Indirect(hdlr).Type().Name()
|
||||||
|
|
||||||
|
var endpoints []*register.Endpoint
|
||||||
|
|
||||||
|
for m := 0; m < typ.NumMethod(); m++ {
|
||||||
|
if e := register.ExtractEndpoint(typ.Method(m)); e != nil {
|
||||||
|
e.Name = name + "." + e.Name
|
||||||
|
|
||||||
|
for k, v := range options.Metadata[e.Name] {
|
||||||
|
e.Metadata[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints = append(endpoints, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rpcHandler{
|
||||||
|
name: name,
|
||||||
|
handler: handler,
|
||||||
|
endpoints: endpoints,
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcHandler) Name() string {
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcHandler) Handler() interface{} {
|
||||||
|
return r.handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcHandler) Endpoints() []*register.Endpoint {
|
||||||
|
return r.endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcHandler) Options() HandlerOptions {
|
||||||
|
return r.opts
|
||||||
|
}
|
199
server/noop.go
199
server/noop.go
@@ -1,11 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v4/register"
|
"go.unistack.org/micro/v4/register"
|
||||||
@@ -24,12 +25,13 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type noopServer struct {
|
type noopServer struct {
|
||||||
h Handler
|
h Handler
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
rsvc *register.Service
|
rsvc *register.Service
|
||||||
handlers map[string]Handler
|
handlers map[string]Handler
|
||||||
exit chan chan error
|
subscribers map[*subscriber][]broker.Subscriber
|
||||||
opts Options
|
exit chan chan error
|
||||||
|
opts Options
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
registered bool
|
registered bool
|
||||||
started bool
|
started bool
|
||||||
@@ -41,6 +43,9 @@ func NewServer(opts ...Option) Server {
|
|||||||
if n.handlers == nil {
|
if n.handlers == nil {
|
||||||
n.handlers = make(map[string]Handler)
|
n.handlers = make(map[string]Handler)
|
||||||
}
|
}
|
||||||
|
if n.subscribers == nil {
|
||||||
|
n.subscribers = make(map[*subscriber][]broker.Subscriber)
|
||||||
|
}
|
||||||
if n.exit == nil {
|
if n.exit == nil {
|
||||||
n.exit = make(chan chan error)
|
n.exit = make(chan chan error)
|
||||||
}
|
}
|
||||||
@@ -66,10 +71,37 @@ func (n *noopServer) Name() string {
|
|||||||
return n.opts.Name
|
return n.opts.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *noopServer) Subscribe(sb Subscriber) error {
|
||||||
|
sub, ok := sb.(*subscriber)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid subscriber: expected *subscriber")
|
||||||
|
} else if len(sub.handlers) == 0 {
|
||||||
|
return fmt.Errorf("invalid subscriber: no handler functions")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateSubscriber(sb); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.Lock()
|
||||||
|
if _, ok = n.subscribers[sub]; ok {
|
||||||
|
n.Unlock()
|
||||||
|
return fmt.Errorf("subscriber %v already exists", sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.subscribers[sub] = nil
|
||||||
|
n.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (n *noopServer) NewHandler(h interface{}, opts ...HandlerOption) Handler {
|
func (n *noopServer) NewHandler(h interface{}, opts ...HandlerOption) Handler {
|
||||||
return newRPCHandler(h, opts...)
|
return newRPCHandler(h, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *noopServer) NewSubscriber(topic string, sb interface{}, opts ...SubscriberOption) Subscriber {
|
||||||
|
return newSubscriber(topic, sb, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
func (n *noopServer) Init(opts ...Option) error {
|
func (n *noopServer) Init(opts ...Option) error {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&n.opts)
|
o(&n.opts)
|
||||||
@@ -78,6 +110,9 @@ func (n *noopServer) Init(opts ...Option) error {
|
|||||||
if n.handlers == nil {
|
if n.handlers == nil {
|
||||||
n.handlers = make(map[string]Handler, 1)
|
n.handlers = make(map[string]Handler, 1)
|
||||||
}
|
}
|
||||||
|
if n.subscribers == nil {
|
||||||
|
n.subscribers = make(map[*subscriber][]broker.Subscriber, 1)
|
||||||
|
}
|
||||||
|
|
||||||
if n.exit == nil {
|
if n.exit == nil {
|
||||||
n.exit = make(chan chan error)
|
n.exit = make(chan chan error)
|
||||||
@@ -123,10 +158,21 @@ func (n *noopServer) Register() error {
|
|||||||
|
|
||||||
sort.Strings(handlerList)
|
sort.Strings(handlerList)
|
||||||
|
|
||||||
endpoints := make([]*register.Endpoint, 0, len(handlerList))
|
subscriberList := make([]*subscriber, 0, len(n.subscribers))
|
||||||
|
for e := range n.subscribers {
|
||||||
|
subscriberList = append(subscriberList, e)
|
||||||
|
}
|
||||||
|
sort.Slice(subscriberList, func(i, j int) bool {
|
||||||
|
return subscriberList[i].topic > subscriberList[j].topic
|
||||||
|
})
|
||||||
|
|
||||||
|
endpoints := make([]*register.Endpoint, 0, len(handlerList)+len(subscriberList))
|
||||||
for _, h := range handlerList {
|
for _, h := range handlerList {
|
||||||
endpoints = append(endpoints, n.handlers[h].Endpoints()...)
|
endpoints = append(endpoints, n.handlers[h].Endpoints()...)
|
||||||
}
|
}
|
||||||
|
for _, e := range subscriberList {
|
||||||
|
endpoints = append(endpoints, e.Endpoints()...)
|
||||||
|
}
|
||||||
n.RUnlock()
|
n.RUnlock()
|
||||||
|
|
||||||
service.Nodes[0].Metadata["protocol"] = "noop"
|
service.Nodes[0].Metadata["protocol"] = "noop"
|
||||||
@@ -156,6 +202,39 @@ func (n *noopServer) Register() error {
|
|||||||
n.Lock()
|
n.Lock()
|
||||||
defer n.Unlock()
|
defer n.Unlock()
|
||||||
|
|
||||||
|
cx := config.Context
|
||||||
|
|
||||||
|
var sub broker.Subscriber
|
||||||
|
|
||||||
|
for sb := range n.subscribers {
|
||||||
|
if sb.Options().Context != nil {
|
||||||
|
cx = sb.Options().Context
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []broker.SubscribeOption{broker.SubscribeContext(cx), broker.SubscribeAutoAck(sb.Options().AutoAck)}
|
||||||
|
if queue := sb.Options().Queue; len(queue) > 0 {
|
||||||
|
opts = append(opts, broker.SubscribeGroup(queue))
|
||||||
|
}
|
||||||
|
|
||||||
|
if sb.Options().Batch {
|
||||||
|
// batch processing handler
|
||||||
|
sub, err = config.Broker.BatchSubscribe(cx, sb.Topic(), n.newBatchSubHandler(sb, config), opts...)
|
||||||
|
} else {
|
||||||
|
// single processing handler
|
||||||
|
sub, err = config.Broker.Subscribe(cx, sb.Topic(), n.newSubHandler(sb, config), opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Logger.V(logger.InfoLevel) {
|
||||||
|
config.Logger.Infof(n.opts.Context, "subscribing to topic: %s", sb.Topic())
|
||||||
|
}
|
||||||
|
|
||||||
|
n.subscribers[sb] = []broker.Subscriber{sub}
|
||||||
|
}
|
||||||
|
|
||||||
n.registered = true
|
n.registered = true
|
||||||
if cacheService {
|
if cacheService {
|
||||||
n.rsvc = service
|
n.rsvc = service
|
||||||
@@ -194,6 +273,33 @@ func (n *noopServer) Deregister() error {
|
|||||||
|
|
||||||
n.registered = false
|
n.registered = false
|
||||||
|
|
||||||
|
cx := config.Context
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for sb, subs := range n.subscribers {
|
||||||
|
for idx := range subs {
|
||||||
|
if sb.Options().Context != nil {
|
||||||
|
cx = sb.Options().Context
|
||||||
|
}
|
||||||
|
|
||||||
|
ncx := cx
|
||||||
|
wg.Add(1)
|
||||||
|
go func(s broker.Subscriber) {
|
||||||
|
defer wg.Done()
|
||||||
|
if config.Logger.V(logger.InfoLevel) {
|
||||||
|
config.Logger.Infof(n.opts.Context, "unsubscribing from topic: %s", s.Topic())
|
||||||
|
}
|
||||||
|
if err := s.Unsubscribe(ncx); err != nil {
|
||||||
|
if config.Logger.V(logger.ErrorLevel) {
|
||||||
|
config.Logger.Errorf(n.opts.Context, "unsubscribing from topic: %s err: %v", s.Topic(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(subs[idx])
|
||||||
|
}
|
||||||
|
n.subscribers[sb] = nil
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
n.Unlock()
|
n.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -230,6 +336,21 @@ func (n *noopServer) Start() error {
|
|||||||
}
|
}
|
||||||
n.Unlock()
|
n.Unlock()
|
||||||
|
|
||||||
|
// only connect if we're subscribed
|
||||||
|
if len(n.subscribers) > 0 {
|
||||||
|
// connect to the broker
|
||||||
|
if err := config.Broker.Connect(config.Context); err != nil {
|
||||||
|
if config.Logger.V(logger.ErrorLevel) {
|
||||||
|
config.Logger.Errorf(n.opts.Context, "broker [%s] connect error: %v", config.Broker.String(), err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Logger.V(logger.InfoLevel) {
|
||||||
|
config.Logger.Infof(n.opts.Context, "broker [%s] Connected to %s", config.Broker.String(), config.Broker.Address())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// use RegisterCheck func before register
|
// use RegisterCheck func before register
|
||||||
// nolint: nestif
|
// nolint: nestif
|
||||||
if err := config.RegisterCheck(config.Context); err != nil {
|
if err := config.RegisterCheck(config.Context); err != nil {
|
||||||
@@ -308,6 +429,16 @@ func (n *noopServer) Start() error {
|
|||||||
|
|
||||||
// close transport
|
// close transport
|
||||||
ch <- nil
|
ch <- nil
|
||||||
|
|
||||||
|
if config.Logger.V(logger.InfoLevel) {
|
||||||
|
config.Logger.Infof(n.opts.Context, "broker [%s] Disconnected from %s", config.Broker.String(), config.Broker.Address())
|
||||||
|
}
|
||||||
|
// disconnect broker
|
||||||
|
if err := config.Broker.Disconnect(config.Context); err != nil {
|
||||||
|
if config.Logger.V(logger.ErrorLevel) {
|
||||||
|
config.Logger.Errorf(n.opts.Context, "broker [%s] disconnect error: %v", config.Broker.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// mark the server as started
|
// mark the server as started
|
||||||
@@ -337,55 +468,3 @@ func (n *noopServer) Stop() error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type rpcHandler struct {
|
|
||||||
opts HandlerOptions
|
|
||||||
handler interface{}
|
|
||||||
name string
|
|
||||||
endpoints []*register.Endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRPCHandler(handler interface{}, opts ...HandlerOption) Handler {
|
|
||||||
options := NewHandlerOptions(opts...)
|
|
||||||
|
|
||||||
typ := reflect.TypeOf(handler)
|
|
||||||
hdlr := reflect.ValueOf(handler)
|
|
||||||
name := reflect.Indirect(hdlr).Type().Name()
|
|
||||||
|
|
||||||
var endpoints []*register.Endpoint
|
|
||||||
|
|
||||||
for m := 0; m < typ.NumMethod(); m++ {
|
|
||||||
if e := register.ExtractEndpoint(typ.Method(m)); e != nil {
|
|
||||||
e.Name = name + "." + e.Name
|
|
||||||
|
|
||||||
for k, v := range options.Metadata[e.Name] {
|
|
||||||
e.Metadata[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints = append(endpoints, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &rpcHandler{
|
|
||||||
name: name,
|
|
||||||
handler: handler,
|
|
||||||
endpoints: endpoints,
|
|
||||||
opts: options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rpcHandler) Name() string {
|
|
||||||
return r.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rpcHandler) Handler() interface{} {
|
|
||||||
return r.handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rpcHandler) Endpoints() []*register.Endpoint {
|
|
||||||
return r.endpoints
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rpcHandler) Options() HandlerOptions {
|
|
||||||
return r.opts
|
|
||||||
}
|
|
||||||
|
104
server/noop_test.go
Normal file
104
server/noop_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package server_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
|
"go.unistack.org/micro/v4/client"
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
"go.unistack.org/micro/v4/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestHandler struct {
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestMessage struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TestHandler) SingleSubHandler(ctx context.Context, msg *codec.Frame) error {
|
||||||
|
// fmt.Printf("msg %s\n", msg.Data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TestHandler) BatchSubHandler(ctxs []context.Context, msgs []*codec.Frame) error {
|
||||||
|
if len(msgs) != 8 {
|
||||||
|
h.t.Fatal("invalid number of messages received")
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(msgs); idx++ {
|
||||||
|
md, _ := metadata.FromIncomingContext(ctxs[idx])
|
||||||
|
_ = md
|
||||||
|
// fmt.Printf("msg md %v\n", md)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoopSub(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
b := broker.NewBroker()
|
||||||
|
|
||||||
|
if err := b.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Connect(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.DefaultLogger.Init(logger.WithLevel(logger.ErrorLevel))
|
||||||
|
s := server.NewServer(
|
||||||
|
server.Broker(b),
|
||||||
|
server.Codec("application/octet-stream", codec.NewCodec()),
|
||||||
|
)
|
||||||
|
if err := s.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := client.NewClient(
|
||||||
|
client.Broker(b),
|
||||||
|
client.Codec("application/octet-stream", codec.NewCodec()),
|
||||||
|
client.ContentType("application/octet-stream"),
|
||||||
|
)
|
||||||
|
if err := c.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := &TestHandler{t: t}
|
||||||
|
|
||||||
|
if err := s.Subscribe(s.NewSubscriber("single_topic", h.SingleSubHandler,
|
||||||
|
server.SubscriberQueue("queue"),
|
||||||
|
)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Subscribe(s.NewSubscriber("batch_topic", h.BatchSubHandler,
|
||||||
|
server.SubscriberQueue("queue"),
|
||||||
|
server.SubscriberBatch(true),
|
||||||
|
)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs := make([]client.Message, 0, 8)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
msgs = append(msgs, c.NewMessage("batch_topic", &codec.Frame{Data: []byte(fmt.Sprintf(`{"name": "test_name %d"}`, i))}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BatchPublish(ctx, msgs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := s.Stop(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v4/logger"
|
"go.unistack.org/micro/v4/logger"
|
||||||
"go.unistack.org/micro/v4/metadata"
|
"go.unistack.org/micro/v4/metadata"
|
||||||
@@ -18,17 +19,6 @@ import (
|
|||||||
"go.unistack.org/micro/v4/util/id"
|
"go.unistack.org/micro/v4/util/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// ServerRequestDurationSeconds specifies meter metric name
|
|
||||||
ServerRequestDurationSeconds = "server_request_duration_seconds"
|
|
||||||
// ServerRequestLatencyMicroseconds specifies meter metric name
|
|
||||||
ServerRequestLatencyMicroseconds = "server_request_latency_microseconds"
|
|
||||||
// ServerRequestTotal specifies meter metric name
|
|
||||||
ServerRequestTotal = "server_request_total"
|
|
||||||
// ServerRequestInflight specifies meter metric name
|
|
||||||
ServerRequestInflight = "server_request_inflight"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Option func
|
// Option func
|
||||||
type Option func(*Options)
|
type Option func(*Options)
|
||||||
|
|
||||||
@@ -36,6 +26,8 @@ type Option func(*Options)
|
|||||||
type Options struct {
|
type Options struct {
|
||||||
// Context holds the external options and can be used for server shutdown
|
// Context holds the external options and can be used for server shutdown
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
// Broker holds the server broker
|
||||||
|
Broker broker.Broker
|
||||||
// Register holds the register
|
// Register holds the register
|
||||||
Register register.Register
|
Register register.Register
|
||||||
// Tracer holds the tracer
|
// Tracer holds the tracer
|
||||||
@@ -46,6 +38,12 @@ type Options struct {
|
|||||||
Meter meter.Meter
|
Meter meter.Meter
|
||||||
// Transport holds the transport
|
// Transport holds the transport
|
||||||
Transport transport.Transport
|
Transport transport.Transport
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Router for requests
|
||||||
|
Router Router
|
||||||
|
*/
|
||||||
|
|
||||||
// Listener may be passed if already created
|
// Listener may be passed if already created
|
||||||
Listener net.Listener
|
Listener net.Listener
|
||||||
// Wait group
|
// Wait group
|
||||||
@@ -70,6 +68,10 @@ type Options struct {
|
|||||||
Advertise string
|
Advertise string
|
||||||
// Version holds the server version
|
// Version holds the server version
|
||||||
Version string
|
Version string
|
||||||
|
// SubWrappers holds the server subscribe wrappers
|
||||||
|
SubWrappers []SubscriberWrapper
|
||||||
|
// BatchSubWrappers holds the server batch subscribe wrappers
|
||||||
|
BatchSubWrappers []BatchSubscriberWrapper
|
||||||
// HdlrWrappers holds the handler wrappers
|
// HdlrWrappers holds the handler wrappers
|
||||||
HdlrWrappers []HandlerWrapper
|
HdlrWrappers []HandlerWrapper
|
||||||
// RegisterAttempts holds the number of register attempts before error
|
// RegisterAttempts holds the number of register attempts before error
|
||||||
@@ -82,7 +84,7 @@ type Options struct {
|
|||||||
MaxConn int
|
MaxConn int
|
||||||
// DeregisterAttempts holds the number of deregister attempts before error
|
// DeregisterAttempts holds the number of deregister attempts before error
|
||||||
DeregisterAttempts int
|
DeregisterAttempts int
|
||||||
// Hooks may contains HandlerWrapper or Server func wrapper
|
// Hooks may contains SubscriberWrapper, HandlerWrapper or Server func wrapper
|
||||||
Hooks options.Hooks
|
Hooks options.Hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ func NewOptions(opts ...Option) Options {
|
|||||||
Logger: logger.DefaultLogger,
|
Logger: logger.DefaultLogger,
|
||||||
Meter: meter.DefaultMeter,
|
Meter: meter.DefaultMeter,
|
||||||
Tracer: tracer.DefaultTracer,
|
Tracer: tracer.DefaultTracer,
|
||||||
|
Broker: broker.DefaultBroker,
|
||||||
Register: register.DefaultRegister,
|
Register: register.DefaultRegister,
|
||||||
Transport: transport.DefaultTransport,
|
Transport: transport.DefaultTransport,
|
||||||
Address: DefaultAddress,
|
Address: DefaultAddress,
|
||||||
@@ -170,6 +173,13 @@ func Advertise(a string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broker to use for pub/sub
|
||||||
|
func Broker(b broker.Broker) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Broker = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Codec to use to encode/decode requests for a given content type
|
// Codec to use to encode/decode requests for a given content type
|
||||||
func Codec(contentType string, c codec.Codec) Option {
|
func Codec(contentType string, c codec.Codec) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -251,6 +261,15 @@ func TLSConfig(t *tls.Config) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// WithRouter sets the request router
|
||||||
|
func WithRouter(r Router) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.Router = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Wait tells the server to wait for requests to finish before exiting
|
// Wait tells the server to wait for requests to finish before exiting
|
||||||
// If `wg` is nil, server only wait for completion of rpc handler.
|
// If `wg` is nil, server only wait for completion of rpc handler.
|
||||||
// For user need finer grained control, pass a concrete `wg` here, server will
|
// For user need finer grained control, pass a concrete `wg` here, server will
|
||||||
@@ -271,6 +290,20 @@ func WrapHandler(w HandlerWrapper) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapSubscriber adds a subscriber Wrapper to a list of options passed into the server
|
||||||
|
func WrapSubscriber(w SubscriberWrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.SubWrappers = append(o.SubWrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapBatchSubscriber adds a batch subscriber Wrapper to a list of options passed into the server
|
||||||
|
func WrapBatchSubscriber(w BatchSubscriberWrapper) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.BatchSubWrappers = append(o.BatchSubWrappers, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MaxConn specifies maximum number of max simultaneous connections to server
|
// MaxConn specifies maximum number of max simultaneous connections to server
|
||||||
func MaxConn(n int) Option {
|
func MaxConn(n int) Option {
|
||||||
return func(o *Options) {
|
return func(o *Options) {
|
||||||
@@ -310,6 +343,41 @@ func NewHandlerOptions(opts ...HandlerOption) HandlerOptions {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscriberOption func
|
||||||
|
type SubscriberOption func(*SubscriberOptions)
|
||||||
|
|
||||||
|
// SubscriberOptions struct
|
||||||
|
type SubscriberOptions struct {
|
||||||
|
// Context holds the external options
|
||||||
|
Context context.Context
|
||||||
|
// Queue holds the subscription queue
|
||||||
|
Queue string
|
||||||
|
// AutoAck flag for auto ack messages after processing
|
||||||
|
AutoAck bool
|
||||||
|
// BodyOnly flag specifies that message without headers
|
||||||
|
BodyOnly bool
|
||||||
|
// Batch flag specifies that message processed in batches
|
||||||
|
Batch bool
|
||||||
|
// BatchSize flag specifies max size of batch
|
||||||
|
BatchSize int
|
||||||
|
// BatchWait flag specifies max wait time for batch filling
|
||||||
|
BatchWait time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubscriberOptions create new SubscriberOptions
|
||||||
|
func NewSubscriberOptions(opts ...SubscriberOption) SubscriberOptions {
|
||||||
|
options := SubscriberOptions{
|
||||||
|
AutoAck: true,
|
||||||
|
Context: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
// EndpointMetadata is a Handler option that allows metadata to be added to
|
// EndpointMetadata is a Handler option that allows metadata to be added to
|
||||||
// individual endpoints.
|
// individual endpoints.
|
||||||
func EndpointMetadata(name string, md metadata.Metadata) HandlerOption {
|
func EndpointMetadata(name string, md metadata.Metadata) HandlerOption {
|
||||||
@@ -317,3 +385,68 @@ func EndpointMetadata(name string, md metadata.Metadata) HandlerOption {
|
|||||||
o.Metadata[name] = metadata.Copy(md)
|
o.Metadata[name] = metadata.Copy(md)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableAutoAck will disable auto acking of messages
|
||||||
|
// after they have been handled.
|
||||||
|
func DisableAutoAck() SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.AutoAck = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberQueue sets the shared queue name distributed messages across subscribers
|
||||||
|
func SubscriberQueue(n string) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.Queue = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberGroup sets the shared group name distributed messages across subscribers
|
||||||
|
func SubscriberGroup(n string) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.Queue = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBodyOnly says broker that message contains raw data with absence of micro broker.Message format
|
||||||
|
func SubscriberBodyOnly(b bool) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.BodyOnly = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberContext set context options to allow broker SubscriberOption passed
|
||||||
|
func SubscriberContext(ctx context.Context) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.Context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberAck control auto ack processing for handler
|
||||||
|
func SubscriberAck(b bool) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.AutoAck = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBatch control batch processing for handler
|
||||||
|
func SubscriberBatch(b bool) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.Batch = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBatchSize control batch filling size for handler
|
||||||
|
// Batch filling max waiting time controlled by SubscriberBatchWait
|
||||||
|
func SubscriberBatchSize(n int) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.BatchSize = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberBatchWait control batch filling wait time for handler
|
||||||
|
func SubscriberBatchWait(td time.Duration) SubscriberOption {
|
||||||
|
return func(o *SubscriberOptions) {
|
||||||
|
o.BatchWait = td
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -78,6 +78,7 @@ func NewRegisterService(s Server) (*register.Service, error) {
|
|||||||
node.Metadata = metadata.Copy(opts.Metadata)
|
node.Metadata = metadata.Copy(opts.Metadata)
|
||||||
|
|
||||||
node.Metadata["server"] = s.String()
|
node.Metadata["server"] = s.String()
|
||||||
|
node.Metadata["broker"] = opts.Broker.String()
|
||||||
node.Metadata["register"] = opts.Register.String()
|
node.Metadata["register"] = opts.Register.String()
|
||||||
|
|
||||||
return ®ister.Service{
|
return ®ister.Service{
|
||||||
|
35
server/request.go
Normal file
35
server/request.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rpcMessage struct {
|
||||||
|
payload interface{}
|
||||||
|
codec codec.Codec
|
||||||
|
header metadata.Metadata
|
||||||
|
topic string
|
||||||
|
contentType string
|
||||||
|
body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcMessage) ContentType() string {
|
||||||
|
return r.contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcMessage) Topic() string {
|
||||||
|
return r.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcMessage) Body() interface{} {
|
||||||
|
return r.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcMessage) Header() metadata.Metadata {
|
||||||
|
return r.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rpcMessage) Codec() codec.Codec {
|
||||||
|
return r.codec
|
||||||
|
}
|
@@ -48,6 +48,10 @@ type Server interface {
|
|||||||
Handle(h Handler) error
|
Handle(h Handler) error
|
||||||
// Create a new handler
|
// Create a new handler
|
||||||
NewHandler(h interface{}, opts ...HandlerOption) Handler
|
NewHandler(h interface{}, opts ...HandlerOption) Handler
|
||||||
|
// Create a new subscriber
|
||||||
|
NewSubscriber(topic string, h interface{}, opts ...SubscriberOption) Subscriber
|
||||||
|
// Register a subscriber
|
||||||
|
Subscribe(s Subscriber) error
|
||||||
// Start the server
|
// Start the server
|
||||||
Start() error
|
Start() error
|
||||||
// Stop the server
|
// Stop the server
|
||||||
@@ -56,6 +60,30 @@ type Server interface {
|
|||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Router handle serving messages
|
||||||
|
type Router interface {
|
||||||
|
// ProcessMessage processes a message
|
||||||
|
ProcessMessage(ctx context.Context, msg Message) error
|
||||||
|
// ServeRequest processes a request to completion
|
||||||
|
ServeRequest(ctx context.Context, req Request, rsp Response) error
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Message is an async message interface
|
||||||
|
type Message interface {
|
||||||
|
// Topic of the message
|
||||||
|
Topic() string
|
||||||
|
// The decoded payload value
|
||||||
|
Body() interface{}
|
||||||
|
// The content type of the payload
|
||||||
|
ContentType() string
|
||||||
|
// The raw headers of the message
|
||||||
|
Header() metadata.Metadata
|
||||||
|
// Codec used to decode the message
|
||||||
|
Codec() codec.Codec
|
||||||
|
}
|
||||||
|
|
||||||
// Request is a synchronous request interface
|
// Request is a synchronous request interface
|
||||||
type Request interface {
|
type Request interface {
|
||||||
// Service name requested
|
// Service name requested
|
||||||
@@ -117,14 +145,25 @@ type Stream interface {
|
|||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// type Greeter struct {}
|
// type Greeter struct {}
|
||||||
|
//
|
||||||
|
// func (g *Greeter) Hello(context, request, response) error {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// func (g *Greeter) Hello(context, request, response) error {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
Name() string
|
Name() string
|
||||||
Handler() interface{}
|
Handler() interface{}
|
||||||
Endpoints() []*register.Endpoint
|
Endpoints() []*register.Endpoint
|
||||||
Options() HandlerOptions
|
Options() HandlerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscriber interface represents a subscription to a given topic using
|
||||||
|
// a specific subscriber function or object with endpoints. It mirrors
|
||||||
|
// the handler in its behaviour.
|
||||||
|
type Subscriber interface {
|
||||||
|
Topic() string
|
||||||
|
Subscriber() interface{}
|
||||||
|
Endpoints() []*register.Endpoint
|
||||||
|
Options() SubscriberOptions
|
||||||
|
}
|
||||||
|
440
server/subscriber.go
Normal file
440
server/subscriber.go
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"go.unistack.org/micro/v4/broker"
|
||||||
|
"go.unistack.org/micro/v4/codec"
|
||||||
|
"go.unistack.org/micro/v4/errors"
|
||||||
|
"go.unistack.org/micro/v4/logger"
|
||||||
|
"go.unistack.org/micro/v4/metadata"
|
||||||
|
"go.unistack.org/micro/v4/register"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
subSig = "func(context.Context, interface{}) error"
|
||||||
|
batchSubSig = "func([]context.Context, []interface{}) error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Precompute the reflect type for error. Can't use error directly
|
||||||
|
// because Typeof takes an empty interface value. This is annoying.
|
||||||
|
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
reqType reflect.Type
|
||||||
|
ctxType reflect.Type
|
||||||
|
method reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type subscriber struct {
|
||||||
|
typ reflect.Type
|
||||||
|
subscriber interface{}
|
||||||
|
topic string
|
||||||
|
endpoints []*register.Endpoint
|
||||||
|
handlers []*handler
|
||||||
|
opts SubscriberOptions
|
||||||
|
rcvr reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this an exported - upper case - name?
|
||||||
|
func isExported(name string) bool {
|
||||||
|
rune, _ := utf8.DecodeRuneInString(name)
|
||||||
|
return unicode.IsUpper(rune)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this type exported or a builtin?
|
||||||
|
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||||
|
for t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
// PkgPath will be non-empty even for an exported type,
|
||||||
|
// so we need to check the type name as well.
|
||||||
|
return isExported(t.Name()) || t.PkgPath() == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSubscriber func signature
|
||||||
|
func ValidateSubscriber(sub Subscriber) error {
|
||||||
|
typ := reflect.TypeOf(sub.Subscriber())
|
||||||
|
var argType reflect.Type
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Func:
|
||||||
|
name := "Func"
|
||||||
|
switch typ.NumIn() {
|
||||||
|
case 2:
|
||||||
|
argType = typ.In(1)
|
||||||
|
if sub.Options().Batch {
|
||||||
|
if argType.Kind() != reflect.Slice {
|
||||||
|
return fmt.Errorf("subscriber %v dont have required signature %s", name, batchSubSig)
|
||||||
|
}
|
||||||
|
if strings.Compare(fmt.Sprintf("%v", argType), "[]interface{}") == 0 {
|
||||||
|
return fmt.Errorf("subscriber %v dont have required signaure %s", name, batchSubSig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s or %s", name, typ.NumIn(), subSig, batchSubSig)
|
||||||
|
}
|
||||||
|
if !isExportedOrBuiltinType(argType) {
|
||||||
|
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
|
||||||
|
}
|
||||||
|
if typ.NumOut() != 1 {
|
||||||
|
return fmt.Errorf("subscriber %v has wrong number of return values: %v require signature %s or %s",
|
||||||
|
name, typ.NumOut(), subSig, batchSubSig)
|
||||||
|
}
|
||||||
|
if returnType := typ.Out(0); returnType != typeOfError {
|
||||||
|
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
hdlr := reflect.ValueOf(sub.Subscriber())
|
||||||
|
name := reflect.Indirect(hdlr).Type().Name()
|
||||||
|
|
||||||
|
for m := 0; m < typ.NumMethod(); m++ {
|
||||||
|
method := typ.Method(m)
|
||||||
|
switch method.Type.NumIn() {
|
||||||
|
case 3:
|
||||||
|
argType = method.Type.In(2)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s or %s",
|
||||||
|
name, method.Name, method.Type.NumIn(), subSig, batchSubSig)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isExportedOrBuiltinType(argType) {
|
||||||
|
return fmt.Errorf("%v argument type not exported: %v", name, argType)
|
||||||
|
}
|
||||||
|
if method.Type.NumOut() != 1 {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"subscriber %v.%v has wrong number of return values: %v require signature %s or %s",
|
||||||
|
name, method.Name, method.Type.NumOut(), subSig, batchSubSig)
|
||||||
|
}
|
||||||
|
if returnType := method.Type.Out(0); returnType != typeOfError {
|
||||||
|
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubscriber(topic string, sub interface{}, opts ...SubscriberOption) Subscriber {
|
||||||
|
var endpoints []*register.Endpoint
|
||||||
|
var handlers []*handler
|
||||||
|
|
||||||
|
options := NewSubscriberOptions(opts...)
|
||||||
|
|
||||||
|
if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func {
|
||||||
|
h := &handler{
|
||||||
|
method: reflect.ValueOf(sub),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ.NumIn() {
|
||||||
|
case 1:
|
||||||
|
h.reqType = typ.In(0)
|
||||||
|
case 2:
|
||||||
|
h.ctxType = typ.In(0)
|
||||||
|
h.reqType = typ.In(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = append(handlers, h)
|
||||||
|
ep := ®ister.Endpoint{
|
||||||
|
Name: "Func",
|
||||||
|
Request: register.ExtractSubValue(typ),
|
||||||
|
Metadata: metadata.New(2),
|
||||||
|
}
|
||||||
|
ep.Metadata.Set("topic", topic)
|
||||||
|
ep.Metadata.Set("subscriber", "true")
|
||||||
|
endpoints = append(endpoints, ep)
|
||||||
|
} else {
|
||||||
|
hdlr := reflect.ValueOf(sub)
|
||||||
|
name := reflect.Indirect(hdlr).Type().Name()
|
||||||
|
|
||||||
|
for m := 0; m < typ.NumMethod(); m++ {
|
||||||
|
method := typ.Method(m)
|
||||||
|
h := &handler{
|
||||||
|
method: method.Func,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method.Type.NumIn() {
|
||||||
|
case 2:
|
||||||
|
h.reqType = method.Type.In(1)
|
||||||
|
case 3:
|
||||||
|
h.ctxType = method.Type.In(1)
|
||||||
|
h.reqType = method.Type.In(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = append(handlers, h)
|
||||||
|
ep := ®ister.Endpoint{
|
||||||
|
Name: name + "." + method.Name,
|
||||||
|
Request: register.ExtractSubValue(method.Type),
|
||||||
|
Metadata: metadata.New(2),
|
||||||
|
}
|
||||||
|
ep.Metadata.Set("topic", topic)
|
||||||
|
ep.Metadata.Set("subscriber", "true")
|
||||||
|
endpoints = append(endpoints, ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &subscriber{
|
||||||
|
rcvr: reflect.ValueOf(sub),
|
||||||
|
typ: reflect.TypeOf(sub),
|
||||||
|
topic: topic,
|
||||||
|
subscriber: sub,
|
||||||
|
handlers: handlers,
|
||||||
|
endpoints: endpoints,
|
||||||
|
opts: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func (n *noopServer) newBatchSubHandler(sb *subscriber, opts Options) broker.BatchHandler {
|
||||||
|
return func(ps broker.Events) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
n.RLock()
|
||||||
|
config := n.opts
|
||||||
|
n.RUnlock()
|
||||||
|
if config.Logger.V(logger.ErrorLevel) {
|
||||||
|
config.Logger.Error(n.opts.Context, "panic recovered: ", r)
|
||||||
|
config.Logger.Error(n.opts.Context, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
err = errors.InternalServerError(n.opts.Name+".subscriber", "panic recovered: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
msgs := make([]Message, 0, len(ps))
|
||||||
|
ctxs := make([]context.Context, 0, len(ps))
|
||||||
|
for _, p := range ps {
|
||||||
|
msg := p.Message()
|
||||||
|
// if we don't have headers, create empty map
|
||||||
|
if msg.Header == nil {
|
||||||
|
msg.Header = metadata.New(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
ct, _ := msg.Header.Get(metadata.HeaderContentType)
|
||||||
|
if len(ct) == 0 {
|
||||||
|
msg.Header.Set(metadata.HeaderContentType, defaultContentType)
|
||||||
|
ct = defaultContentType
|
||||||
|
}
|
||||||
|
hdr := metadata.Copy(msg.Header)
|
||||||
|
topic, _ := msg.Header.Get(metadata.HeaderTopic)
|
||||||
|
ctxs = append(ctxs, metadata.NewIncomingContext(sb.opts.Context, hdr))
|
||||||
|
msgs = append(msgs, &rpcMessage{
|
||||||
|
topic: topic,
|
||||||
|
contentType: ct,
|
||||||
|
header: msg.Header,
|
||||||
|
body: msg.Body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
results := make(chan error, len(sb.handlers))
|
||||||
|
|
||||||
|
for i := 0; i < len(sb.handlers); i++ {
|
||||||
|
handler := sb.handlers[i]
|
||||||
|
|
||||||
|
var req reflect.Value
|
||||||
|
|
||||||
|
switch handler.reqType.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
req = reflect.New(handler.reqType.Elem())
|
||||||
|
default:
|
||||||
|
req = reflect.New(handler.reqType.Elem()).Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
reqType := handler.reqType
|
||||||
|
var cf codec.Codec
|
||||||
|
for _, msg := range msgs {
|
||||||
|
cf, err = n.newCodec(msg.ContentType())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rb := reflect.New(req.Type().Elem())
|
||||||
|
if err = cf.ReadBody(bytes.NewReader(msg.(*rpcMessage).body), rb.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg.(*rpcMessage).codec = cf
|
||||||
|
msg.(*rpcMessage).payload = rb.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := func(ctxs []context.Context, ms []Message) error {
|
||||||
|
var vals []reflect.Value
|
||||||
|
if sb.typ.Kind() != reflect.Func {
|
||||||
|
vals = append(vals, sb.rcvr)
|
||||||
|
}
|
||||||
|
if handler.ctxType != nil {
|
||||||
|
vals = append(vals, reflect.ValueOf(ctxs))
|
||||||
|
}
|
||||||
|
payloads := reflect.MakeSlice(reqType, 0, len(ms))
|
||||||
|
for _, m := range ms {
|
||||||
|
payloads = reflect.Append(payloads, reflect.ValueOf(m.Body()))
|
||||||
|
}
|
||||||
|
vals = append(vals, payloads)
|
||||||
|
|
||||||
|
returnValues := handler.method.Call(vals)
|
||||||
|
if rerr := returnValues[0].Interface(); rerr != nil {
|
||||||
|
return rerr.(error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(opts.BatchSubWrappers); i > 0; i-- {
|
||||||
|
fn = opts.BatchSubWrappers[i-1](fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.wg != nil {
|
||||||
|
n.wg.Add(1)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if n.wg != nil {
|
||||||
|
defer n.wg.Done()
|
||||||
|
}
|
||||||
|
results <- fn(ctxs, msgs)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
for i := 0; i < len(sb.handlers); i++ {
|
||||||
|
if rerr := <-results; rerr != nil {
|
||||||
|
errors = append(errors, rerr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errors) > 0 {
|
||||||
|
err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
|
func (n *noopServer) newSubHandler(sb *subscriber, opts Options) broker.Handler {
|
||||||
|
return func(p broker.Event) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
n.RLock()
|
||||||
|
config := n.opts
|
||||||
|
n.RUnlock()
|
||||||
|
if config.Logger.V(logger.ErrorLevel) {
|
||||||
|
config.Logger.Error(n.opts.Context, "panic recovered: ", r)
|
||||||
|
config.Logger.Error(n.opts.Context, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
err = errors.InternalServerError(n.opts.Name+".subscriber", "panic recovered: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
msg := p.Message()
|
||||||
|
// if we don't have headers, create empty map
|
||||||
|
if msg.Header == nil {
|
||||||
|
msg.Header = metadata.New(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
ct := msg.Header["Content-Type"]
|
||||||
|
if len(ct) == 0 {
|
||||||
|
msg.Header.Set(metadata.HeaderContentType, defaultContentType)
|
||||||
|
ct = defaultContentType
|
||||||
|
}
|
||||||
|
cf, err := n.newCodec(ct)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr := metadata.New(len(msg.Header))
|
||||||
|
for k, v := range msg.Header {
|
||||||
|
if k == "Content-Type" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hdr.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := metadata.NewIncomingContext(sb.opts.Context, hdr)
|
||||||
|
|
||||||
|
results := make(chan error, len(sb.handlers))
|
||||||
|
|
||||||
|
for i := 0; i < len(sb.handlers); i++ {
|
||||||
|
handler := sb.handlers[i]
|
||||||
|
|
||||||
|
var isVal bool
|
||||||
|
var req reflect.Value
|
||||||
|
|
||||||
|
if handler.reqType.Kind() == reflect.Ptr {
|
||||||
|
req = reflect.New(handler.reqType.Elem())
|
||||||
|
} else {
|
||||||
|
req = reflect.New(handler.reqType)
|
||||||
|
isVal = true
|
||||||
|
}
|
||||||
|
if isVal {
|
||||||
|
req = req.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cf.ReadBody(bytes.NewBuffer(msg.Body), req.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := func(ctx context.Context, msg Message) error {
|
||||||
|
var vals []reflect.Value
|
||||||
|
if sb.typ.Kind() != reflect.Func {
|
||||||
|
vals = append(vals, sb.rcvr)
|
||||||
|
}
|
||||||
|
if handler.ctxType != nil {
|
||||||
|
vals = append(vals, reflect.ValueOf(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
vals = append(vals, reflect.ValueOf(msg.Body()))
|
||||||
|
|
||||||
|
returnValues := handler.method.Call(vals)
|
||||||
|
if rerr := returnValues[0].Interface(); rerr != nil {
|
||||||
|
return rerr.(error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(opts.SubWrappers); i > 0; i-- {
|
||||||
|
fn = opts.SubWrappers[i-1](fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.wg != nil {
|
||||||
|
n.wg.Add(1)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
if n.wg != nil {
|
||||||
|
defer n.wg.Done()
|
||||||
|
}
|
||||||
|
cerr := fn(ctx, &rpcMessage{
|
||||||
|
topic: sb.topic,
|
||||||
|
contentType: ct,
|
||||||
|
payload: req.Interface(),
|
||||||
|
header: msg.Header,
|
||||||
|
})
|
||||||
|
results <- cerr
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
var errors []string
|
||||||
|
for i := 0; i < len(sb.handlers); i++ {
|
||||||
|
if rerr := <-results; rerr != nil {
|
||||||
|
errors = append(errors, rerr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errors) > 0 {
|
||||||
|
err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subscriber) Topic() string {
|
||||||
|
return s.topic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subscriber) Subscriber() interface{} {
|
||||||
|
return s.subscriber
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subscriber) Endpoints() []*register.Endpoint {
|
||||||
|
return s.endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *subscriber) Options() SubscriberOptions {
|
||||||
|
return s.opts
|
||||||
|
}
|
@@ -9,9 +9,25 @@ import (
|
|||||||
// request and response types.
|
// request and response types.
|
||||||
type HandlerFunc func(ctx context.Context, req Request, rsp interface{}) error
|
type HandlerFunc func(ctx context.Context, req Request, rsp interface{}) error
|
||||||
|
|
||||||
|
// SubscriberFunc represents a single method of a subscriber. It's used primarily
|
||||||
|
// for the wrappers. What's handed to the actual method is the concrete
|
||||||
|
// publication message.
|
||||||
|
type SubscriberFunc func(ctx context.Context, msg Message) error
|
||||||
|
|
||||||
|
// BatchSubscriberFunc represents a single method of a subscriber. It's used primarily
|
||||||
|
// for the wrappers. What's handed to the actual method is the concrete
|
||||||
|
// publication message. This func used by batch subscribers
|
||||||
|
type BatchSubscriberFunc func(ctxs []context.Context, msgs []Message) error
|
||||||
|
|
||||||
// HandlerWrapper wraps the HandlerFunc and returns the equivalent
|
// HandlerWrapper wraps the HandlerFunc and returns the equivalent
|
||||||
type HandlerWrapper func(HandlerFunc) HandlerFunc
|
type HandlerWrapper func(HandlerFunc) HandlerFunc
|
||||||
|
|
||||||
|
// SubscriberWrapper wraps the SubscriberFunc and returns the equivalent
|
||||||
|
type SubscriberWrapper func(SubscriberFunc) SubscriberFunc
|
||||||
|
|
||||||
|
// BatchSubscriberWrapper wraps the SubscriberFunc and returns the equivalent
|
||||||
|
type BatchSubscriberWrapper func(BatchSubscriberFunc) BatchSubscriberFunc
|
||||||
|
|
||||||
// StreamWrapper wraps a Stream interface and returns the equivalent.
|
// StreamWrapper wraps a Stream interface and returns the equivalent.
|
||||||
// Because streams exist for the lifetime of a method invocation this
|
// Because streams exist for the lifetime of a method invocation this
|
||||||
// is a convenient way to wrap a Stream as its in use for trace, monitoring,
|
// is a convenient way to wrap a Stream as its in use for trace, monitoring,
|
||||||
|
@@ -66,6 +66,11 @@ func RegisterHandler(s server.Server, h interface{}, opts ...server.HandlerOptio
|
|||||||
return s.Handle(s.NewHandler(h, opts...))
|
return s.Handle(s.NewHandler(h, opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterSubscriber is syntactic sugar for registering a subscriber
|
||||||
|
func RegisterSubscriber(topic string, s server.Server, h interface{}, opts ...server.SubscriberOption) error {
|
||||||
|
return s.Subscribe(s.NewSubscriber(topic, h, opts...))
|
||||||
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
opts Options
|
opts Options
|
||||||
@@ -83,7 +88,6 @@ func (s *service) Name() string {
|
|||||||
// Init initialises options. Additionally it calls cmd.Init
|
// Init initialises options. Additionally it calls cmd.Init
|
||||||
// which parses command line flags. cmd.Init is only called
|
// which parses command line flags. cmd.Init is only called
|
||||||
// on first Init.
|
// on first Init.
|
||||||
//
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (s *service) Init(opts ...Option) error {
|
func (s *service) Init(opts ...Option) error {
|
||||||
var err error
|
var err error
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package micro
|
package micro
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -64,6 +65,41 @@ func TestRegisterHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRegisterSubscriber(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
topic string
|
||||||
|
s server.Server
|
||||||
|
h interface{}
|
||||||
|
opts []server.SubscriberOption
|
||||||
|
}
|
||||||
|
h := func(_ context.Context, _ interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "RegisterSubscriber",
|
||||||
|
args: args{
|
||||||
|
topic: "test",
|
||||||
|
s: server.DefaultServer,
|
||||||
|
h: h,
|
||||||
|
opts: nil,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := RegisterSubscriber(tt.args.topic, tt.args.s, tt.args.h, tt.args.opts...); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("RegisterSubscriber() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewService(t *testing.T) {
|
func TestNewService(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
opts []Option
|
opts []Option
|
||||||
@@ -186,6 +222,7 @@ func Test_service_Options(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_service_Broker(t *testing.T) {
|
func Test_service_Broker(t *testing.T) {
|
||||||
|
b := broker.NewBroker()
|
||||||
type fields struct {
|
type fields struct {
|
||||||
opts Options
|
opts Options
|
||||||
}
|
}
|
||||||
@@ -201,12 +238,12 @@ func Test_service_Broker(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "service.Broker",
|
name: "service.Broker",
|
||||||
fields: fields{
|
fields: fields{
|
||||||
opts: Options{Brokers: []broker.Broker{broker.DefaultBroker}},
|
opts: Options{Brokers: []broker.Broker{b}},
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
names: []string{"noop"},
|
names: []string{"noop"},
|
||||||
},
|
},
|
||||||
want: broker.DefaultBroker,
|
want: b,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Tracer = (*noopTracer)(nil)
|
|
||||||
|
|
||||||
type noopTracer struct {
|
type noopTracer struct {
|
||||||
opts Options
|
opts Options
|
||||||
}
|
}
|
||||||
@@ -23,10 +21,6 @@ func (t *noopTracer) Start(ctx context.Context, name string, opts ...SpanOption)
|
|||||||
return NewSpanContext(ctx, span), span
|
return NewSpanContext(ctx, span), span
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *noopTracer) Flush(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *noopTracer) Init(opts ...Option) error {
|
func (t *noopTracer) Init(opts ...Option) error {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&t.opts)
|
o(&t.opts)
|
||||||
|
@@ -16,8 +16,6 @@ type Tracer interface {
|
|||||||
Init(...Option) error
|
Init(...Option) error
|
||||||
// Start a trace
|
// Start a trace
|
||||||
Start(ctx context.Context, name string, opts ...SpanOption) (context.Context, Span)
|
Start(ctx context.Context, name string, opts ...SpanOption) (context.Context, Span)
|
||||||
// Flush flushes spans
|
|
||||||
Flush(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Span interface {
|
type Span interface {
|
||||||
|
@@ -58,7 +58,6 @@ func IsLocal(addr string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract returns a real ip
|
// Extract returns a real ip
|
||||||
//
|
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func Extract(addr string) (string, error) {
|
func Extract(addr string) (string, error) {
|
||||||
// if addr specified then its returned
|
// if addr specified then its returned
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var osStderrMu sync.Mutex
|
|
||||||
|
|
||||||
var OrigStderr = func() *os.File {
|
|
||||||
fd, err := dupFD(os.Stderr.Fd())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.NewFile(fd, os.Stderr.Name())
|
|
||||||
}()
|
|
@@ -1,40 +0,0 @@
|
|||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrorPattern = regexp.MustCompile(`"error"`)
|
|
||||||
|
|
||||||
func TestRedirect(t *testing.T) {
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan string)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
_, _ = io.Copy(buf, r)
|
|
||||||
ch <- buf.String()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = RedirectStderr(w); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Stderr.Write([]byte(`test redirect`))
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
r.Close()
|
|
||||||
str := <-ch
|
|
||||||
if ErrorPattern.MatchString(str) {
|
|
||||||
t.Fatal(fmt.Errorf(str))
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,48 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// dupFD is used to initialize OrigStderr (see stderr_redirect.go).
|
|
||||||
func dupFD(fd uintptr) (uintptr, error) {
|
|
||||||
// Warning: failing to set FD_CLOEXEC causes the duplicated file descriptor
|
|
||||||
// to leak into subprocesses created by exec.Command. If the file descriptor
|
|
||||||
// is a pipe, these subprocesses will hold the pipe open (i.e., prevent
|
|
||||||
// EOF), potentially beyond the lifetime of this process.
|
|
||||||
//
|
|
||||||
// This can break go test's timeouts. go test usually spawns a test process
|
|
||||||
// with its stdin and stderr streams hooked up to pipes; if the test process
|
|
||||||
// times out, it sends a SIGKILL and attempts to read stdin and stderr to
|
|
||||||
// completion. If the test process has itself spawned long-lived
|
|
||||||
// subprocesses that hold references to the stdin or stderr pipes, go test
|
|
||||||
// will hang until the subprocesses exit, rather defeating the purpose of
|
|
||||||
// a timeout.
|
|
||||||
nfd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uintptr(nfd), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedirectStderr is used to redirect internal writes to fd 2 to the
|
|
||||||
// specified file. This is needed to ensure that harcoded writes to fd
|
|
||||||
// 2 by e.g. the Go runtime are redirected to a log file of our
|
|
||||||
// choosing.
|
|
||||||
//
|
|
||||||
// We also override os.Stderr for those other parts of Go which use
|
|
||||||
// that and not fd 2 directly.
|
|
||||||
func RedirectStderr(f *os.File) error {
|
|
||||||
osStderrMu.Lock()
|
|
||||||
defer osStderrMu.Unlock()
|
|
||||||
if err := unix.Dup2(int(f.Fd()), unix.Stderr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
os.Stderr = f
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
package io
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
// dupFD is used to initialize OrigStderr (see stderr_redirect.go).
|
|
||||||
func dupFD(fd uintptr) (uintptr, error) {
|
|
||||||
// Adapted from https://github.com/golang/go/blob/go1.8/src/syscall/exec_windows.go#L303.
|
|
||||||
p := windows.CurrentProcess()
|
|
||||||
var h windows.Handle
|
|
||||||
return uintptr(h), windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, true, windows.DUPLICATE_SAME_ACCESS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedirectStderr is used to redirect internal writes to the error
|
|
||||||
// handle to the specified file. This is needed to ensure that
|
|
||||||
// harcoded writes to the error handle by e.g. the Go runtime are
|
|
||||||
// redirected to a log file of our choosing.
|
|
||||||
//
|
|
||||||
// We also override os.Stderr for those other parts of Go which use
|
|
||||||
// that and not fd 2 directly.
|
|
||||||
func RedirectStderr(f *os.File) error {
|
|
||||||
osStderrMu.Lock()
|
|
||||||
defer osStderrMu.Unlock()
|
|
||||||
if err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(f.Fd())); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
os.Stderr = f
|
|
||||||
return nil
|
|
||||||
}
|
|
@@ -493,14 +493,13 @@ func btSplitter(str string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queryToMap turns something like a[b][c]=4 into
|
// queryToMap turns something like a[b][c]=4 into
|
||||||
//
|
// map[string]interface{}{
|
||||||
// map[string]interface{}{
|
// "a": map[string]interface{}{
|
||||||
// "a": map[string]interface{}{
|
// "b": map[string]interface{}{
|
||||||
// "b": map[string]interface{}{
|
// "c": 4,
|
||||||
// "c": 4,
|
// },
|
||||||
// },
|
// },
|
||||||
// },
|
// }
|
||||||
// }
|
|
||||||
func queryToMap(param string) (map[string]interface{}, error) {
|
func queryToMap(param string) (map[string]interface{}, error) {
|
||||||
rawKey, rawValue, err := splitKeyAndValue(param)
|
rawKey, rawValue, err := splitKeyAndValue(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
25
util/test/sqlmock_test.go
Normal file
25
util/test/sqlmock_test.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_NewSQLRowsFromFile(t *testing.T) {
|
||||||
|
db, c, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows, err := NewSQLRowsFromFile(c, "testdata/Call.csv")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(fmt.Sprintf("%#+v", rows), `cols:[]string{"DepAgrId", "DepAgrNum", "DepAgrDate", "DepAgrCloseDate", "AccCur", "MainFinaccNum", "MainFinaccName", "MainFinaccId", "MainFinaccOpenDt", "DepAgrStatus", "MainFinaccBal", "DepartCode", "CardAccId"}`) {
|
||||||
|
t.Fatal("invalid cols after import csv")
|
||||||
|
}
|
||||||
|
}
|
@@ -1,212 +1,28 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"errors"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
"go.unistack.org/micro/v4/client"
|
"go.unistack.org/micro/v4/client"
|
||||||
"go.unistack.org/micro/v4/codec"
|
"go.unistack.org/micro/v4/codec"
|
||||||
"go.unistack.org/micro/v4/errors"
|
|
||||||
"go.unistack.org/micro/v4/metadata"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrUnknownContentType = fmt.Errorf("unknown content type")
|
var ErrUnknownContentType = errors.New("unknown content type")
|
||||||
|
|
||||||
type Extension struct {
|
type Extension struct {
|
||||||
Ext []string
|
Ext []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var ExtToTypes = map[string][]string{
|
||||||
// ExtToTypes map file extension to content type
|
"json": {"application/json", "application/grpc+json"},
|
||||||
ExtToTypes = map[string][]string{
|
"yaml": {"application/yaml", "application/yml", "text/yaml", "text/yml"},
|
||||||
"json": {"application/json", "application/grpc+json"},
|
"yml": {"application/yaml", "application/yml", "text/yaml", "text/yml"},
|
||||||
"yaml": {"application/yaml", "application/yml", "text/yaml", "text/yml"},
|
"proto": {"application/grpc", "application/grpc+proto", "application/proto"},
|
||||||
"yml": {"application/yaml", "application/yml", "text/yaml", "text/yml"},
|
|
||||||
"proto": {"application/grpc", "application/grpc+proto", "application/proto"},
|
|
||||||
}
|
|
||||||
// DefaultExts specifies default file extensions to load data
|
|
||||||
DefaultExts = []string{"csv", "json", "yaml", "yml", "proto"}
|
|
||||||
// Codecs map to detect codec for test file or request content type
|
|
||||||
Codecs map[string]codec.Codec
|
|
||||||
|
|
||||||
// ResponseCompareFunc used to compare actual response with test case data
|
|
||||||
ResponseCompareFunc = func(expectRsp []byte, testRsp interface{}, expectCodec codec.Codec, testCodec codec.Codec) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
expectMap := make(map[string]interface{})
|
|
||||||
if err = expectCodec.Unmarshal(expectRsp, &expectMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testMap := make(map[string]interface{})
|
|
||||||
switch v := testRsp.(type) {
|
|
||||||
case *codec.Frame:
|
|
||||||
if err = testCodec.Unmarshal(v.Data, &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
case *errors.Error:
|
|
||||||
if err = expectCodec.Unmarshal([]byte(v.Error()), &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
case error:
|
|
||||||
st, ok := status.FromError(v)
|
|
||||||
if !ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
me := errors.Parse(st.Message())
|
|
||||||
if me.Code != 0 {
|
|
||||||
if err = expectCodec.Unmarshal([]byte(me.Error()), &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for _, se := range st.Details() {
|
|
||||||
switch ne := se.(type) {
|
|
||||||
case proto.Message:
|
|
||||||
buf, err := testCodec.Marshal(ne)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal err: %w", err)
|
|
||||||
}
|
|
||||||
if err = testCodec.Unmarshal(buf, &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return st.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case interface{ GRPCStatus() *status.Status }:
|
|
||||||
st := v.GRPCStatus()
|
|
||||||
me := errors.Parse(st.Message())
|
|
||||||
if me.Code != 0 {
|
|
||||||
if err = expectCodec.Unmarshal([]byte(me.Error()), &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case *status.Status:
|
|
||||||
me := errors.Parse(v.Message())
|
|
||||||
if me.Code != 0 {
|
|
||||||
if err = expectCodec.Unmarshal([]byte(me.Error()), &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for _, se := range v.Details() {
|
|
||||||
switch ne := se.(type) {
|
|
||||||
case proto.Message:
|
|
||||||
buf, err := testCodec.Marshal(ne)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal err: %w", err)
|
|
||||||
}
|
|
||||||
if err = testCodec.Unmarshal(buf, &testMap); err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal err: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return v.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(expectMap, testMap) {
|
|
||||||
return fmt.Errorf("test: %s != rsp: %s", expectMap, testMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func FromCSVString(columns []*sqlmock.Column, rows *sqlmock.Rows, s string) *sqlmock.Rows {
|
|
||||||
res := strings.NewReader(strings.TrimSpace(s))
|
|
||||||
csvReader := csv.NewReader(res)
|
|
||||||
|
|
||||||
for {
|
|
||||||
res, err := csvReader.Read()
|
|
||||||
if err != nil || res == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var row []driver.Value
|
|
||||||
for i, v := range res {
|
|
||||||
item := CSVColumnParser(strings.TrimSpace(v))
|
|
||||||
if null, nullOk := columns[i].IsNullable(); null && nullOk && item == nil {
|
|
||||||
row = append(row, nil)
|
|
||||||
} else {
|
|
||||||
row = append(row, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
rows = rows.AddRow(row...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
func CSVColumnParser(s string) []byte {
|
|
||||||
switch {
|
|
||||||
case strings.ToLower(s) == "null":
|
|
||||||
return nil
|
|
||||||
case s == "":
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []byte(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResponseFromFile(rspfile string) (*codec.Frame, error) {
|
|
||||||
rspbuf, err := os.ReadFile(rspfile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &codec.Frame{Data: rspbuf}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCodec(codecs map[string]codec.Codec, ext string) (codec.Codec, error) {
|
|
||||||
var c codec.Codec
|
|
||||||
if cts, ok := ExtToTypes[ext]; ok {
|
|
||||||
for _, t := range cts {
|
|
||||||
if c, ok = codecs[t]; ok {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrUnknownContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContentType(codecs map[string]codec.Codec, ext string) (string, error) {
|
|
||||||
if cts, ok := ExtToTypes[ext]; ok {
|
|
||||||
for _, t := range cts {
|
|
||||||
if _, ok = codecs[t]; ok {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", ErrUnknownContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExt(name string) string {
|
|
||||||
ext := filepath.Ext(name)
|
|
||||||
if len(ext) > 0 && ext[0] == '.' {
|
|
||||||
ext = ext[1:]
|
|
||||||
}
|
|
||||||
return ext
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNameWithoutExt(name string) string {
|
|
||||||
return strings.TrimSuffix(name, filepath.Ext(name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRequestFromFile(c client.Client, reqfile string) (client.Request, error) {
|
func NewRequestFromFile(c client.Client, reqfile string) (client.Request, error) {
|
||||||
@@ -216,14 +32,23 @@ func NewRequestFromFile(c client.Client, reqfile string) (client.Request, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
endpoint := path.Base(path.Dir(reqfile))
|
endpoint := path.Base(path.Dir(reqfile))
|
||||||
if idx := strings.Index(endpoint, "_"); idx > 0 {
|
ext := path.Ext(reqfile)
|
||||||
endpoint = endpoint[idx+1:]
|
if len(ext) > 0 && ext[0] == '.' {
|
||||||
|
ext = ext[1:]
|
||||||
}
|
}
|
||||||
ext := getExt(reqfile)
|
|
||||||
|
|
||||||
ct, err := getContentType(c.Options().Codecs, ext)
|
var ct string
|
||||||
if err != nil {
|
if cts, ok := ExtToTypes[ext]; ok {
|
||||||
return nil, err
|
for _, t := range cts {
|
||||||
|
if _, ok = c.Options().Codecs[t]; ok {
|
||||||
|
ct = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ct == "" {
|
||||||
|
return nil, ErrUnknownContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
req := c.NewRequest("test", endpoint, &codec.Frame{Data: reqbuf}, client.RequestContentType(ct))
|
req := c.NewRequest("test", endpoint, &codec.Frame{Data: reqbuf}, client.RequestContentType(ct))
|
||||||
@@ -231,275 +56,26 @@ func NewRequestFromFile(c client.Client, reqfile string) (client.Request, error)
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SQLFromFile(m sqlmock.Sqlmock, name string) error {
|
func NewSQLRowsFromFile(c sqlmock.Sqlmock, file string) (*sqlmock.Rows, error) {
|
||||||
fp, err := os.Open(name)
|
fp, err := os.Open(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer fp.Close()
|
defer fp.Close()
|
||||||
return SQLFromReader(m, fp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SQLFromBytes(m sqlmock.Sqlmock, buf []byte) error {
|
r := csv.NewReader(fp)
|
||||||
return SQLFromReader(m, bytes.NewReader(buf))
|
r.Comma = '\t'
|
||||||
}
|
r.Comment = '#'
|
||||||
|
|
||||||
func SQLFromString(m sqlmock.Sqlmock, buf string) error {
|
records, err := r.ReadAll()
|
||||||
return SQLFromReader(m, strings.NewReader(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
func SQLFromReader(m sqlmock.Sqlmock, r io.Reader) error {
|
|
||||||
var rows *sqlmock.Rows
|
|
||||||
var exp *sqlmock.ExpectedQuery
|
|
||||||
var columns []*sqlmock.Column
|
|
||||||
|
|
||||||
br := bufio.NewReader(r)
|
|
||||||
|
|
||||||
for {
|
|
||||||
s, err := br.ReadString('\n')
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return err
|
|
||||||
} else if err == io.EOF && len(s) == 0 {
|
|
||||||
if rows != nil && exp != nil {
|
|
||||||
exp.WillReturnRows(rows)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if s[0] != '#' {
|
|
||||||
r := csv.NewReader(strings.NewReader(s))
|
|
||||||
r.Comma = ','
|
|
||||||
var records [][]string
|
|
||||||
records, err = r.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if rows == nil && len(columns) > 0 {
|
|
||||||
rows = m.NewRowsWithColumnDefinition(columns...)
|
|
||||||
} else {
|
|
||||||
for idx := 0; idx < len(records); idx++ {
|
|
||||||
if len(columns) == 0 {
|
|
||||||
return fmt.Errorf("csv file not valid, does not have %q line", "# columns ")
|
|
||||||
}
|
|
||||||
rows = FromCSVString(columns, rows, strings.Join(records[idx], ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if rows != nil {
|
|
||||||
exp.WillReturnRows(rows)
|
|
||||||
rows = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(strings.ToLower(s[2:]), "columns"):
|
|
||||||
for _, field := range strings.Split(s[2+len("columns")+1:], ",") {
|
|
||||||
args := strings.Split(field, "|")
|
|
||||||
|
|
||||||
column := sqlmock.NewColumn(args[0]).Nullable(false)
|
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
for _, arg := range args {
|
|
||||||
switch arg {
|
|
||||||
case "BOOLEAN", "BOOL":
|
|
||||||
column = column.OfType("BOOL", false)
|
|
||||||
case "NUMBER", "DECIMAL":
|
|
||||||
column = column.OfType("DECIMAL", float64(0.0)).WithPrecisionAndScale(10, 4)
|
|
||||||
case "VARCHAR":
|
|
||||||
column = column.OfType("VARCHAR", nil)
|
|
||||||
case "NULL":
|
|
||||||
column = column.Nullable(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
columns = append(columns, column)
|
|
||||||
}
|
|
||||||
case strings.HasPrefix(strings.ToLower(s[2:]), "begin"):
|
|
||||||
m.ExpectBegin()
|
|
||||||
case strings.HasPrefix(strings.ToLower(s[2:]), "commit"):
|
|
||||||
m.ExpectCommit()
|
|
||||||
case strings.HasPrefix(strings.ToLower(s[2:]), "rollback"):
|
|
||||||
m.ExpectRollback()
|
|
||||||
case strings.HasPrefix(strings.ToLower(s[2:]), "exec "):
|
|
||||||
m.ExpectExec(s[2+len("exec "):])
|
|
||||||
case strings.HasPrefix(strings.ToLower(s[2:]), "query "):
|
|
||||||
exp = m.ExpectQuery(s[2+len("query "):])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(ctx context.Context, c client.Client, m sqlmock.Sqlmock, dir string, exts []string) error {
|
|
||||||
tcases, err := GetCases(dir, exts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
g, gctx := errgroup.WithContext(ctx)
|
rows := c.NewRows(records[0])
|
||||||
if !strings.Contains(dir, "parallel") {
|
for idx := 1; idx < len(records); idx++ {
|
||||||
g.SetLimit(1)
|
rows.FromCSVString(strings.Join(records[idx], ";"))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tcase := range tcases {
|
return rows, nil
|
||||||
|
|
||||||
for _, dbfile := range tcase.dbfiles {
|
|
||||||
if err = SQLFromFile(m, dbfile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tc := tcase
|
|
||||||
g.Go(func() error {
|
|
||||||
var xrid string
|
|
||||||
var gerr error
|
|
||||||
|
|
||||||
treq, err := NewRequestFromFile(c, tc.reqfile)
|
|
||||||
if err != nil {
|
|
||||||
gerr = fmt.Errorf("failed to read request from file %s err: %w", tc.reqfile, err)
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
|
|
||||||
xrid = fmt.Sprintf("%s-%d", treq.Endpoint(), time.Now().Unix())
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if gerr == nil {
|
|
||||||
fmt.Printf("test %s xrid: %s status: success\n", filepath.Dir(tc.reqfile), xrid)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("test %s xrid: %s status: failure error: %v\n", filepath.Dir(tc.reqfile), xrid, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
data := &codec.Frame{}
|
|
||||||
md := metadata.New(1)
|
|
||||||
md.Set("X-Request-Id", xrid)
|
|
||||||
cerr := c.Call(metadata.NewOutgoingContext(gctx, md), treq, data, client.WithContentType(treq.ContentType()))
|
|
||||||
|
|
||||||
var rspfile string
|
|
||||||
|
|
||||||
if tc.errfile != "" {
|
|
||||||
rspfile = tc.errfile
|
|
||||||
} else if tc.rspfile != "" {
|
|
||||||
rspfile = tc.rspfile
|
|
||||||
} else {
|
|
||||||
gerr = fmt.Errorf("errfile and rspfile is empty")
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
|
|
||||||
expectRsp, err := NewResponseFromFile(rspfile)
|
|
||||||
if err != nil {
|
|
||||||
gerr = fmt.Errorf("failed to read response from file %s err: %w", rspfile, err)
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
|
|
||||||
testCodec, err := getCodec(Codecs, getExt(tc.reqfile))
|
|
||||||
if err != nil {
|
|
||||||
gerr = fmt.Errorf("failed to get response file codec err: %w", err)
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
|
|
||||||
expectCodec, err := getCodec(Codecs, getExt(rspfile))
|
|
||||||
if err != nil {
|
|
||||||
gerr = fmt.Errorf("failed to get response file codec err: %w", err)
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
|
|
||||||
if cerr == nil && tc.errfile != "" {
|
|
||||||
gerr = fmt.Errorf("expected err %s not happened", expectRsp.Data)
|
|
||||||
return gerr
|
|
||||||
} else if cerr != nil && tc.errfile != "" {
|
|
||||||
if err = ResponseCompareFunc(expectRsp.Data, cerr, expectCodec, testCodec); err != nil {
|
|
||||||
gerr = err
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
} else if cerr != nil && tc.errfile == "" {
|
|
||||||
gerr = cerr
|
|
||||||
return gerr
|
|
||||||
} else if cerr == nil && tc.errfile == "" {
|
|
||||||
if err = ResponseCompareFunc(expectRsp.Data, data, expectCodec, testCodec); err != nil {
|
|
||||||
gerr = err
|
|
||||||
return gerr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
cf, err := getCodec(c.Options().Codecs, getExt(tc.rspfile))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return g.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Case struct {
|
|
||||||
dbfiles []string
|
|
||||||
reqfile string
|
|
||||||
rspfile string
|
|
||||||
errfile string
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCases(dir string, exts []string) ([]Case, error) {
|
|
||||||
var tcases []Case
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if len(entries) == 0 && err != nil {
|
|
||||||
return tcases, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if exts == nil {
|
|
||||||
exts = DefaultExts
|
|
||||||
}
|
|
||||||
|
|
||||||
var dirs []string
|
|
||||||
var dbfiles []string
|
|
||||||
var reqfile, rspfile, errfile string
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
dirs = append(dirs, filepath.Join(dir, entry.Name()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if info, err := entry.Info(); err != nil {
|
|
||||||
return tcases, err
|
|
||||||
} else if !info.Mode().IsRegular() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ext := range exts {
|
|
||||||
if getExt(entry.Name()) == ext {
|
|
||||||
name := getNameWithoutExt(entry.Name())
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(name, "_db"):
|
|
||||||
dbfiles = append(dbfiles, filepath.Join(dir, entry.Name()))
|
|
||||||
case strings.HasSuffix(name, "_req"):
|
|
||||||
reqfile = filepath.Join(dir, entry.Name())
|
|
||||||
case strings.HasSuffix(name, "_rsp"):
|
|
||||||
rspfile = filepath.Join(dir, entry.Name())
|
|
||||||
case strings.HasSuffix(name, "_err"):
|
|
||||||
errfile = filepath.Join(dir, entry.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqfile != "" && (rspfile != "" || errfile != "") {
|
|
||||||
tcases = append(tcases, Case{dbfiles: dbfiles, reqfile: reqfile, rspfile: rspfile, errfile: errfile})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir = range dirs {
|
|
||||||
ntcases, err := GetCases(dir, exts)
|
|
||||||
if len(ntcases) == 0 && err != nil {
|
|
||||||
return tcases, err
|
|
||||||
} else if len(ntcases) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tcases = append(tcases, ntcases...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tcases, nil
|
|
||||||
}
|
}
|
||||||
|
@@ -1,72 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_SQLFromFile(t *testing.T) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
db, c, err := sqlmock.New()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
if err = SQLFromFile(c, "testdata/result/01_firstcase/Call_db.csv"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := tx.QueryContext(ctx, "select * from test;")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for rows.Next() {
|
|
||||||
var id int64
|
|
||||||
var name string
|
|
||||||
err = rows.Scan(&id, &name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if id != 1 || name != "test" {
|
|
||||||
t.Fatalf("invalid rows %v %v", id, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err = c.ExpectationsWereMet(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_GetCases(t *testing.T) {
|
|
||||||
files, err := GetCases("testdata/", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
|
||||||
t.Fatalf("no files matching")
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := len(files); n != 1 {
|
|
||||||
t.Fatalf("invalid number of test cases %d", n)
|
|
||||||
}
|
|
||||||
}
|
|
3
util/test/testdata/Call.csv
vendored
Normal file
3
util/test/testdata/Call.csv
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DepAgrId DepAgrNum DepAgrDate DepAgrCloseDate AccCur MainFinaccNum MainFinaccName MainFinaccId MainFinaccOpenDt DepAgrStatus MainFinaccBal DepartCode CardAccId
|
||||||
|
RBO223352456529 006-21/19-013946448 06112019 "" RUR 42306810521060000015 Депозит по договору № 006-21/19-013946448 220667379184 06112019 WORK 1100000 006-21
|
||||||
|
RBO151198718263 000-500/18-009684290 19062018 "" RUR 40817810001004518898 Текущий счет по договору № 144086737921 19062018 WORK 14091.13 000-500 144121164578
|
|
@@ -1,6 +0,0 @@
|
|||||||
# begin
|
|
||||||
# query select \* from test;
|
|
||||||
# columns id|VARCHAR,name|VARCHAR
|
|
||||||
id,name
|
|
||||||
1,test
|
|
||||||
# commit
|
|
|
@@ -1 +0,0 @@
|
|||||||
{}
|
|
@@ -1 +0,0 @@
|
|||||||
{}
|
|
@@ -1,5 +1,6 @@
|
|||||||
package text
|
package text
|
||||||
|
|
||||||
|
|
||||||
func DetectEncoding(text string) map[string]int {
|
func DetectEncoding(text string) map[string]int {
|
||||||
charsets := map[string]int{
|
charsets := map[string]int{
|
||||||
"UTF-8": 0,
|
"UTF-8": 0,
|
||||||
@@ -76,7 +77,7 @@ func DetectEncoding(text string) map[string]int {
|
|||||||
charsets["MAC"] += uppercase
|
charsets["MAC"] += uppercase
|
||||||
}
|
}
|
||||||
|
|
||||||
last_simb = char
|
last_simb = char
|
||||||
}
|
}
|
||||||
|
|
||||||
return charsets
|
return charsets
|
||||||
|
@@ -2,9 +2,7 @@ package time
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,42 +13,39 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||||||
return 0, fmt.Errorf(`time: invalid duration "` + s + `"`)
|
return 0, fmt.Errorf(`time: invalid duration "` + s + `"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var p int
|
//var sb strings.Builder
|
||||||
var hours int
|
/*
|
||||||
loop:
|
for i, r := range s {
|
||||||
for i, r := range s {
|
switch r {
|
||||||
switch r {
|
case 'd':
|
||||||
case 's', 'm':
|
n, err := strconv.Atoi(s[idx:i])
|
||||||
break loop
|
if err != nil {
|
||||||
case 'h':
|
return 0, errors.New("time: invalid duration " + s)
|
||||||
d, err := strconv.Atoi(s[p:i])
|
}
|
||||||
if err != nil {
|
s[idx:i] = fmt.Sprintf("%d", n*24)
|
||||||
return 0, errors.New("time: invalid duration " + s)
|
default:
|
||||||
|
sb.WriteRune(r)
|
||||||
}
|
}
|
||||||
hours += d
|
}
|
||||||
p = i + 1
|
*/
|
||||||
case 'd':
|
var td time.Duration
|
||||||
d, err := strconv.Atoi(s[p:i])
|
var err error
|
||||||
if err != nil {
|
switch s[len(s)-1] {
|
||||||
return 0, errors.New("time: invalid duration " + s)
|
case 's', 'm', 'h':
|
||||||
}
|
td, err = time.ParseDuration(s)
|
||||||
hours += d * 24
|
case 'd':
|
||||||
p = i + 1
|
if td, err = time.ParseDuration(s[:len(s)-1] + "h"); err == nil {
|
||||||
case 'y':
|
td *= 24
|
||||||
n, err := strconv.Atoi(s[p:i])
|
}
|
||||||
if err != nil {
|
case 'y':
|
||||||
return 0, errors.New("time: invalid duration " + s)
|
if td, err = time.ParseDuration(s[:len(s)-1] + "h"); err == nil {
|
||||||
}
|
year := time.Date(time.Now().Year(), time.December, 31, 0, 0, 0, 0, time.Local)
|
||||||
var d int
|
days := year.YearDay()
|
||||||
for j := n - 1; j >= 0; j-- {
|
td *= 24 * time.Duration(days)
|
||||||
d += time.Date(time.Now().Year()+j, time.December, 31, 0, 0, 0, 0, time.Local).YearDay()
|
|
||||||
}
|
|
||||||
hours += d * 24
|
|
||||||
p = i + 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return time.ParseDuration(fmt.Sprintf("%dh%s", hours, s[p:]))
|
return td, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
@@ -67,7 +62,7 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
|
|||||||
*d = Duration(time.Duration(value))
|
*d = Duration(time.Duration(value))
|
||||||
return nil
|
return nil
|
||||||
case string:
|
case string:
|
||||||
dv, err := ParseDuration(value)
|
dv, err := time.ParseDuration(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -23,34 +23,27 @@ func TestUnmarshalJSON(t *testing.T) {
|
|||||||
TTL Duration `json:"ttl"`
|
TTL Duration `json:"ttl"`
|
||||||
}
|
}
|
||||||
v := &str{}
|
v := &str{}
|
||||||
var err error
|
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(`{"ttl":"10ms"}`), v)
|
err := json.Unmarshal([]byte(`{"ttl":"10ms"}`), v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if v.TTL != 10000000 {
|
} else if v.TTL != 10000000 {
|
||||||
t.Fatalf("invalid duration %v != 10000000", v.TTL)
|
t.Fatalf("invalid duration %v != 10000000", v.TTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(`{"ttl":"1y"}`), v)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if v.TTL != 31536000000000000 {
|
|
||||||
t.Fatalf("invalid duration %v != 31536000000000000", v.TTL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDuration(t *testing.T) {
|
func TestParseDuration(t *testing.T) {
|
||||||
var td time.Duration
|
var td time.Duration
|
||||||
var err error
|
var err error
|
||||||
|
t.Skip()
|
||||||
td, err = ParseDuration("14d4h")
|
td, err = ParseDuration("14d4h")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseDuration error: %v", err)
|
t.Fatalf("ParseDuration error: %v", err)
|
||||||
}
|
}
|
||||||
if td.String() != "340h0m0s" {
|
if td.String() != "336h0m0s" {
|
||||||
t.Fatalf("ParseDuration 14d != 340h0m0s : %s", td.String())
|
t.Fatalf("ParseDuration 14d != 336h0m0s : %s", td.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
td, err = ParseDuration("1y")
|
td, err = ParseDuration("1y")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseDuration error: %v", err)
|
t.Fatalf("ParseDuration error: %v", err)
|
||||||
|
Reference in New Issue
Block a user