Compare commits

..

27 Commits

Author SHA1 Message Date
9c22ae5384 hasql-v3 (#408)
All checks were successful
coverage / build (push) Successful in 3m5s
test / test (push) Successful in 4m4s
initial ha sql support

Co-authored-by: vtolstov <vtolstov@users.noreply.github.com>
Reviewed-on: #408
Co-authored-by: Vasiliy Tolstov <v.tolstov@unistack.org>
Co-committed-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-09-21 00:09:34 +03:00
vtolstov
16bad9a0cd Apply Code Coverage Badge 2025-09-19 14:34:36 +00:00
vtolstov
3c779b248f Apply Code Coverage Badge
Some checks failed
coverage / build (push) Successful in 2m49s
test / test (push) Failing after 17m18s
2025-05-20 10:27:56 +00:00
bbc7512054 v3 (#404)
Some checks failed
coverage / build (push) Successful in 4m17s
test / test (push) Failing after 17m8s
## Pull Request template
Please, go through these steps before clicking submit on this PR.

1. Give a descriptive title to your PR.
2. Provide a description of your changes.
3. Make sure you have some relevant tests.
4. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable).

**PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING**

Reviewed-on: #404
Co-authored-by: Evstigneev Denis <danteevstigneev@yandex.ru>
Co-committed-by: Evstigneev Denis <danteevstigneev@yandex.ru>
2025-05-20 13:24:52 +03:00
github-actions[bot]
dd810e4ae0 Merge remote-tracking branch 'upstream/v3' into v3 2025-05-01 16:18:41 +00:00
236ed47ab1 update ci (#214) 2025-05-01 19:15:11 +03:00
vtolstov
909bcf51a4 Apply Code Coverage Badge 2025-05-01 16:14:42 +00:00
9c24001f52 move wrapper.sql to hook/sql (#400)
All checks were successful
coverage / build (push) Successful in 4m25s
test / test (push) Successful in 7m28s
move micro-wrapper-sql to core micro

Co-authored-by: Vasiliy Tolstov <v.tolstov@unistack.org>
Reviewed-on: #400
Co-authored-by: Evstigneev Denis <danteevstigneev@yandex.ru>
Co-committed-by: Evstigneev Denis <danteevstigneev@yandex.ru>
2025-05-01 19:12:33 +03:00
680cd6f708 [v3] fix flatten map util function (#212)
All checks were successful
coverage / build (push) Successful in 2m59s
test / test (push) Successful in 4m15s
* Apply Code Coverage Badge
* add the fixed version of FlattenMap() and corresponding tests
* clenaup readme


---------

Co-authored-by: pugnack <pugnack@users.noreply.github.com>
2025-04-27 22:22:24 +03:00
vtolstov
3fcf3bef6d Apply Code Coverage Badge 2025-04-27 10:38:55 +00:00
vtolstov
0ecd6199d4 Apply Code Coverage Badge
All checks were successful
coverage / build (push) Successful in 2m21s
test / test (push) Successful in 3m8s
2025-04-27 10:37:37 +00:00
14a30fb6a7 fix panic on shutdown caused by double channel close (#208) 2025-04-27 13:37:00 +03:00
7c613072df [v3] rename .gitea to .github (#207)
Some checks failed
coverage / build (push) Failing after 50s
test / test (push) Has been cancelled
* rename .gitea to .github
* attempt to fix lint/test job
* attempt to fix coverage job
2025-04-27 13:32:44 +03:00
vtolstov
c55e212270 Apply Code Coverage Badge 2025-04-22 17:34:26 +00:00
100bc006bb Merge pull request 'move hooks' (#397) from devstigneev/micro:move_hooks_v3 into v3
All checks were successful
coverage / build (push) Successful in 1m26s
test / test (push) Successful in 2m21s
Reviewed-on: #397
2025-04-22 20:33:47 +03:00
5dbfe8a7a6 Delete SECURITY.md 2025-04-22 15:53:06 +03:00
6be077dbe8 Update README.md 2025-04-22 15:05:07 +03:00
b4878211ee Update README.md 2025-04-22 15:02:01 +03:00
ec9178c6d4 Update README.md 2025-04-22 14:27:14 +03:00
ae63d44866 move hooks
Some checks failed
lint / lint (pull_request) Failing after 1m52s
test / test (pull_request) Successful in 4m37s
coverage / build (pull_request) Failing after 6m59s
2025-04-22 09:41:22 +03:00
883e79216a Update README.md 2025-04-22 08:43:53 +03:00
fa636ef6a9 tracer: add IsRecording to span interface
All checks were successful
coverage / build (push) Successful in 3m33s
test / test (push) Successful in 5m17s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-04-14 00:03:04 +03:00
cdb81a9ba3 remove debug
Some checks failed
coverage / build (push) Failing after 3m6s
test / test (push) Successful in 4m55s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-03-06 22:19:10 +03:00
413c6cc2f0 logger: fixup WithAddFields
All checks were successful
coverage / build (push) Successful in 1m58s
test / test (push) Successful in 5m39s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-02-21 18:12:27 +03:00
vtolstov
f56bd70136 Apply Code Coverage Badge 2025-02-06 13:22:17 +00:00
b51b4107a8 logger/slog: fixup stacktrace
All checks were successful
coverage / build (push) Successful in 1m27s
test / test (push) Successful in 2m54s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-02-06 16:21:29 +03:00
2067c9de6b util/buffer: add new seeker implementation
Some checks failed
coverage / build (push) Failing after 1m35s
test / test (push) Successful in 3m11s
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2025-02-06 15:19:09 +03:00
153 changed files with 5716 additions and 3402 deletions

View File

@@ -1,94 +0,0 @@
name: sync
on:
schedule:
- cron: '*/5 * * * *'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
sync:
if: github.server_url != 'https://github.com'
runs-on: ubuntu-latest
steps:
- name: init
run: |
git config --global user.email "vtolstov <vtolstov@users.noreply.github.com>"
git config --global user.name "github-actions[bot]"
echo "machine git.unistack.org login vtolstov password ${{ secrets.TOKEN_GITEA }}" >> /root/.netrc
echo "machine github.com login vtolstov password ${{ secrets.TOKEN_GITHUB }}" >> /root/.netrc
- name: check master
id: check_master
run: |
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/master | cut -f1)
echo "src_hash=$src_hash"
echo "dst_hash=$dst_hash"
if [ "$src_hash" != "$dst_hash" ]; then
echo "sync_needed=true" >> $GITHUB_OUTPUT
else
echo "sync_needed=false" >> $GITHUB_OUTPUT
fi
- name: sync master
if: steps.check_master.outputs.sync_needed == 'true'
run: |
git clone --filter=blob:none --filter=tree:0 --branch master --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
cd repo
git remote add --no-tags --fetch --track master upstream https://github.com/${GITHUB_REPOSITORY}
git pull --rebase upstream master
git push upstream master --progress
git push origin master --progress
cd ../
rm -rf repo
- name: check v3
id: check_v3
run: |
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v3 | cut -f1)
echo "src_hash=$src_hash"
echo "dst_hash=$dst_hash"
if [ "$src_hash" != "$dst_hash" ]; then
echo "sync_needed=true" >> $GITHUB_OUTPUT
else
echo "sync_needed=false" >> $GITHUB_OUTPUT
fi
- name: sync v3
if: steps.check_v3.outputs.sync_needed == 'true'
run: |
git clone --filter=blob:none --filter=tree:0 --branch v3 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
cd repo
git remote add --no-tags --fetch --track v3 upstream https://github.com/${GITHUB_REPOSITORY}
git pull --rebase upstream v3
git push upstream v3 --progress
git push origin v3 --progress
cd ../
rm -rf repo
- name: check v4
id: check_v4
run: |
src_hash=$(git ls-remote https://github.com/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
dst_hash=$(git ls-remote ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} refs/heads/v4 | cut -f1)
echo "src_hash=$src_hash"
echo "dst_hash=$dst_hash"
if [ "$src_hash" != "$dst_hash" ]; then
echo "sync_needed=true" >> $GITHUB_OUTPUT
else
echo "sync_needed=false" >> $GITHUB_OUTPUT
fi
- name: sync v4
if: steps.check_v4.outputs.sync_needed == 'true'
run: |
git clone --filter=blob:none --filter=tree:0 --branch v4 --single-branch ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY} repo
cd repo
git remote add --no-tags --fetch --track v4 upstream https://github.com/${GITHUB_REPOSITORY}
git pull --rebase upstream v4
git push upstream v4 --progress
git push origin v4 --progress
cd ../
rm -rf repo

View File

@@ -1,28 +1,28 @@
# Micro # Micro
![Coverage](https://img.shields.io/badge/Coverage-34.1%25-yellow) ![Coverage](https://img.shields.io/badge/Coverage-33.6%25-yellow)
[![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Doc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go.unistack.org/micro/v4?tab=overview) [![Doc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/go.unistack.org/micro/v3?tab=overview)
[![Status](https://git.unistack.org/unistack-org/micro/actions/workflows/job_tests.yml/badge.svg?branch=v4)](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av4+event%3Apush) [![Status](https://git.unistack.org/unistack-org/micro/actions/workflows/job_tests.yml/badge.svg?branch=v3)](https://git.unistack.org/unistack-org/micro/actions?query=workflow%3Abuild+branch%3Av3+event%3Apush)
[![Lint](https://goreportcard.com/badge/go.unistack.org/micro/v4)](https://goreportcard.com/report/go.unistack.org/micro/v4) [![Lint](https://goreportcard.com/badge/go.unistack.org/micro/v3)](https://goreportcard.com/report/go.unistack.org/micro/v3)
Micro is a standard library for microservices. Micro is a standard library for microservices.
## Overview ## Overview
Micro provides the core requirements for distributed systems development including RPC and Event driven communication. Micro provides the core requirements for distributed systems development including SYNC and ASYNC communication.
## Features ## Features
Micro abstracts away the details of distributed systems. Here are the main features. Micro abstracts away the details of distributed systems. Main features:
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application - **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
level config from any source such as env vars, cmdline, file, consul, vault... You can merge the sources and even define fallbacks. level config from any source such as env vars, cmdline, file, consul, vault, etc... You can merge the sources and even define fallbacks.
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and - **Data Storage** - A simple data store interface to read, write and delete records. It includes support for memory, file and
s3. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework. s3. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service - **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. development.
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type - **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client

View File

@@ -6,8 +6,8 @@ import (
"errors" "errors"
"time" "time"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
) )
// DefaultBroker default memory broker // DefaultBroker default memory broker
@@ -18,10 +18,6 @@ var (
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 invalid Message passed
ErrInvalidMessage = errors.New("invalid message")
// ErrInvalidHandler returns when subscriber passed to Subscribe
ErrInvalidHandler = errors.New("invalid handler, ony func(Message) error and func([]Message) error supported")
// DefaultGracefulTimeout // DefaultGracefulTimeout
DefaultGracefulTimeout = 5 * time.Second DefaultGracefulTimeout = 5 * time.Second
) )
@@ -40,12 +36,14 @@ 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 create new broker message to publish.
NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...MessageOption) (Message, error)
// Publish message to broker topic // Publish message to broker topic
Publish(ctx context.Context, topic string, messages ...Message) 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
// Live returns broker liveness // Live returns broker liveness
@@ -57,27 +55,72 @@ type Broker interface {
} }
type ( type (
FuncPublish func(ctx context.Context, topic string, messages ...Message) error FuncPublish func(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error
HookPublish func(next FuncPublish) FuncPublish HookPublish func(next FuncPublish) FuncPublish
FuncSubscribe func(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) FuncBatchPublish func(ctx context.Context, msgs []*Message, opts ...PublishOption) error
HookBatchPublish func(next FuncBatchPublish) FuncBatchPublish
FuncSubscribe func(ctx context.Context, topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
HookSubscribe func(next FuncSubscribe) FuncSubscribe HookSubscribe func(next FuncSubscribe) FuncSubscribe
FuncBatchSubscribe func(ctx context.Context, topic string, h BatchHandler, opts ...SubscribeOption) (Subscriber, error)
HookBatchSubscribe func(next FuncBatchSubscribe) FuncBatchSubscribe
) )
// 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.
// 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 {
// Context return context.Context for event
Context() context.Context Context() context.Context
// Topic returns message destination topic. // Topic returns event topic
Topic() string Topic() string
// Header returns message headers. // Message returns broker message
Header() metadata.Metadata Message() *Message
// Body returns broker message []byte slice // Ack acknowledge message
Body() []byte
// Unmarshal try to decode message body to dst.
// This is helper method that uses codec.Unmarshal.
Unmarshal(dst interface{}, opts ...codec.Option) error
// Ack acknowledge message if supported.
Ack() error Ack() error
// Error returns message error (like decoding errors or some other)
Error() error
// SetError set event processing error
SetError(err error)
}
// Message is used to transfer data
type Message struct {
// Header contains message metadata
Header metadata.Metadata
// Body contains message body
Body codec.RawMessage
}
// NewMessage create broker message with topic filled
func NewMessage(topic string) *Message {
m := &Message{Header: metadata.New(2)}
m.Header.Set(metadata.HeaderTopic, topic)
return m
} }
// Subscriber is a convenience return type for the Subscribe method // Subscriber is a convenience return type for the Subscribe method

View File

@@ -42,16 +42,6 @@ func SetSubscribeOption(k, v interface{}) SubscribeOption {
} }
} }
// SetMessageOption returns a function to setup a context with given value
func SetMessageOption(k, v interface{}) MessageOption {
return func(o *MessageOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetOption returns a function to setup a context with given value // SetOption returns a function to setup a context with given value
func SetOption(k, v interface{}) Option { func SetOption(k, v interface{}) Option {
return func(o *Options) { return func(o *Options) {
@@ -61,3 +51,13 @@ func SetOption(k, v interface{}) Option {
o.Context = context.WithValue(o.Context, k, v) o.Context = context.WithValue(o.Context, k, v)
} }
} }
// SetPublishOption returns a function to setup a context with given value
func SetPublishOption(k, v interface{}) PublishOption {
return func(o *PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

View File

@@ -49,6 +49,17 @@ func TestSetSubscribeOption(t *testing.T) {
} }
} }
func TestSetPublishOption(t *testing.T) {
type key struct{}
o := SetPublishOption(key{}, "test")
opts := &PublishOptions{}
o(opts)
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
t.Fatal("SetPublishOption not works")
}
}
func TestSetOption(t *testing.T) { func TestSetOption(t *testing.T) {
type key struct{} type key struct{}
o := SetOption(key{}, "test") o := SetOption(key{}, "test")

View File

@@ -2,104 +2,66 @@ package broker
import ( import (
"context" "context"
"strings"
"sync" "sync"
"go.unistack.org/micro/v4/broker" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v4/options" maddr "go.unistack.org/micro/v3/util/addr"
maddr "go.unistack.org/micro/v4/util/addr" "go.unistack.org/micro/v3/util/id"
"go.unistack.org/micro/v4/util/id" mnet "go.unistack.org/micro/v3/util/net"
mnet "go.unistack.org/micro/v4/util/net" "go.unistack.org/micro/v3/util/rand"
"go.unistack.org/micro/v4/util/rand"
) )
type Broker struct { type memoryBroker struct {
funcPublish broker.FuncPublish funcPublish broker.FuncPublish
funcBatchPublish broker.FuncBatchPublish
funcSubscribe broker.FuncSubscribe funcSubscribe broker.FuncSubscribe
subscribers map[string][]*Subscriber funcBatchSubscribe broker.FuncBatchSubscribe
subscribers map[string][]*memorySubscriber
addr string addr string
opts broker.Options opts broker.Options
mu sync.RWMutex sync.RWMutex
connected bool connected bool
} }
type memoryMessage struct { type memoryEvent struct {
c codec.Codec err error
message interface{}
topic string topic string
ctx context.Context opts broker.Options
body []byte
hdr metadata.Metadata
opts broker.MessageOptions
} }
func (m *memoryMessage) Ack() error { type memorySubscriber struct {
return nil
}
func (m *memoryMessage) Body() []byte {
return m.body
}
func (m *memoryMessage) Header() metadata.Metadata {
return m.hdr
}
func (m *memoryMessage) Context() context.Context {
return m.ctx
}
func (m *memoryMessage) Topic() string {
return ""
}
func (m *memoryMessage) Unmarshal(dst interface{}, opts ...codec.Option) error {
return m.c.Unmarshal(m.body, dst)
}
type Subscriber struct {
ctx context.Context ctx context.Context
exit chan bool exit chan bool
handler interface{} handler broker.Handler
batchhandler broker.BatchHandler
id string id string
topic string topic string
opts broker.SubscribeOptions opts broker.SubscribeOptions
} }
func (b *Broker) newCodec(ct string) (codec.Codec, error) { func (m *memoryBroker) Options() broker.Options {
if idx := strings.IndexRune(ct, ';'); idx >= 0 { return m.opts
ct = ct[:idx]
}
b.mu.RLock()
c, ok := b.opts.Codecs[ct]
b.mu.RUnlock()
if ok {
return c, nil
}
return nil, codec.ErrUnknownContentType
} }
func (b *Broker) Options() broker.Options { func (m *memoryBroker) Address() string {
return b.opts return m.addr
} }
func (b *Broker) Address() string { func (m *memoryBroker) Connect(ctx context.Context) error {
return b.addr
}
func (b *Broker) Connect(ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
} }
b.mu.Lock() m.Lock()
defer b.mu.Unlock() defer m.Unlock()
if b.connected { if m.connected {
return nil return nil
} }
@@ -113,131 +75,156 @@ func (b *Broker) Connect(ctx context.Context) error {
// set addr with port // set addr with port
addr = mnet.HostPort(addr, 10000+i) addr = mnet.HostPort(addr, 10000+i)
b.addr = addr m.addr = addr
b.connected = true m.connected = true
return nil return nil
} }
func (b *Broker) Disconnect(ctx context.Context) error { func (m *memoryBroker) Disconnect(ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
} }
b.mu.Lock() m.Lock()
defer b.mu.Unlock() defer m.Unlock()
if !b.connected { if !m.connected {
return nil return nil
} }
b.connected = false m.connected = false
return nil return nil
} }
func (b *Broker) Init(opts ...broker.Option) error { func (m *memoryBroker) Init(opts ...broker.Option) error {
for _, o := range opts { for _, o := range opts {
o(&b.opts) o(&m.opts)
} }
b.funcPublish = b.fnPublish m.funcPublish = m.fnPublish
b.funcSubscribe = b.fnSubscribe m.funcBatchPublish = m.fnBatchPublish
m.funcSubscribe = m.fnSubscribe
m.funcBatchSubscribe = m.fnBatchSubscribe
b.opts.Hooks.EachPrev(func(hook options.Hook) { m.opts.Hooks.EachPrev(func(hook options.Hook) {
switch h := hook.(type) { switch h := hook.(type) {
case broker.HookPublish: case broker.HookPublish:
b.funcPublish = h(b.funcPublish) m.funcPublish = h(m.funcPublish)
case broker.HookBatchPublish:
m.funcBatchPublish = h(m.funcBatchPublish)
case broker.HookSubscribe: case broker.HookSubscribe:
b.funcSubscribe = h(b.funcSubscribe) m.funcSubscribe = h(m.funcSubscribe)
case broker.HookBatchSubscribe:
m.funcBatchSubscribe = h(m.funcBatchSubscribe)
} }
}) })
return nil return nil
} }
func (b *Broker) NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...broker.MessageOption) (broker.Message, error) { func (m *memoryBroker) Publish(ctx context.Context, topic string, msg *broker.Message, opts ...broker.PublishOption) error {
options := broker.NewMessageOptions(opts...) return m.funcPublish(ctx, topic, msg, opts...)
if options.ContentType == "" {
options.ContentType = b.opts.ContentType
}
m := &memoryMessage{ctx: ctx, hdr: hdr, opts: options}
c, err := b.newCodec(m.opts.ContentType)
if err == nil {
m.body, err = c.Marshal(body)
}
if err != nil {
return nil, err
} }
return m, nil func (m *memoryBroker) fnPublish(ctx context.Context, topic string, msg *broker.Message, opts ...broker.PublishOption) error {
msg.Header.Set(metadata.HeaderTopic, topic)
return m.publish(ctx, []*broker.Message{msg}, opts...)
} }
func (b *Broker) Publish(ctx context.Context, topic string, messages ...broker.Message) error { func (m *memoryBroker) BatchPublish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
return b.funcPublish(ctx, topic, messages...) return m.funcBatchPublish(ctx, msgs, opts...)
} }
func (b *Broker) fnPublish(ctx context.Context, topic string, messages ...broker.Message) error { func (m *memoryBroker) fnBatchPublish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
return b.publish(ctx, topic, messages...) return m.publish(ctx, msgs, opts...)
} }
func (b *Broker) publish(ctx context.Context, topic string, messages ...broker.Message) error { func (m *memoryBroker) publish(ctx context.Context, msgs []*broker.Message, opts ...broker.PublishOption) error {
b.mu.RLock() m.RLock()
if !b.connected { if !m.connected {
b.mu.RUnlock() m.RUnlock()
return broker.ErrNotConnected return broker.ErrNotConnected
} }
b.mu.RUnlock() m.RUnlock()
var err error
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
options := broker.NewPublishOptions(opts...)
msgTopicMap := make(map[string]broker.Events)
for _, v := range msgs {
p := &memoryEvent{opts: m.opts}
if m.opts.Codec == nil || options.BodyOnly {
p.topic, _ = v.Header.Get(metadata.HeaderTopic)
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[p.topic] = append(msgTopicMap[p.topic], p)
} }
b.mu.RLock() beh := m.opts.BatchErrorHandler
subs, ok := b.subscribers[topic] eh := m.opts.ErrorHandler
b.mu.RUnlock()
for t, ms := range msgTopicMap {
m.RLock()
subs, ok := m.subscribers[t]
m.RUnlock()
if !ok { if !ok {
return nil continue
} }
var err error
for _, sub := range subs { for _, sub := range subs {
switch s := sub.handler.(type) { if sub.opts.BatchErrorHandler != nil {
default: beh = sub.opts.BatchErrorHandler
if b.opts.Logger.V(logger.ErrorLevel) {
b.opts.Logger.Error(ctx, "broker handler error", broker.ErrInvalidHandler)
} }
case func(broker.Message) error: if sub.opts.ErrorHandler != nil {
for _, message := range messages { eh = sub.opts.ErrorHandler
msg, ok := message.(*memoryMessage) }
if !ok {
if b.opts.Logger.V(logger.ErrorLevel) { switch {
b.opts.Logger.Error(ctx, "broker handler error", broker.ErrInvalidMessage) // batch processing
case sub.batchhandler != nil:
if err = sub.batchhandler(ms); err != nil {
ms.SetError(err)
if beh != nil {
_ = beh(ms)
} else if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, err.Error())
}
} else if sub.opts.AutoAck {
if err = ms.Ack(); err != nil {
m.opts.Logger.Error(m.opts.Context, "broker ack error", err)
} }
} }
msg.topic = topic // single processing
if err = s(msg); err == nil && sub.opts.AutoAck { case sub.handler != nil:
err = msg.Ack() for _, p := range ms {
if err = sub.handler(p); err != nil {
p.SetError(err)
if eh != nil {
_ = eh(p)
} else if m.opts.Logger.V(logger.ErrorLevel) {
m.opts.Logger.Error(m.opts.Context, "broker handler error", err)
} }
if err != nil { } else if sub.opts.AutoAck {
if b.opts.Logger.V(logger.ErrorLevel) { if err = p.Ack(); err != nil {
b.opts.Logger.Error(ctx, "broker handler error", err) m.opts.Logger.Error(m.opts.Context, "broker ack error", err)
} }
} }
} }
case func([]broker.Message) error:
if err = s(messages); err == nil && sub.opts.AutoAck {
for _, message := range messages {
err = message.Ack()
if err != nil {
if b.opts.Logger.V(logger.ErrorLevel) {
b.opts.Logger.Error(ctx, "broker handler error", err)
}
}
} }
} }
} }
@@ -246,21 +233,17 @@ func (b *Broker) publish(ctx context.Context, topic string, messages ...broker.M
return nil return nil
} }
func (b *Broker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...broker.SubscribeOption) (broker.Subscriber, error) { func (m *memoryBroker) BatchSubscribe(ctx context.Context, topic string, handler broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
return b.funcSubscribe(ctx, topic, handler, opts...) return m.funcBatchSubscribe(ctx, topic, handler, opts...)
} }
func (b *Broker) fnSubscribe(ctx context.Context, topic string, handler interface{}, opts ...broker.SubscribeOption) (broker.Subscriber, error) { func (m *memoryBroker) fnBatchSubscribe(ctx context.Context, topic string, handler broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
if err := broker.IsValidHandler(handler); err != nil { m.RLock()
return nil, err if !m.connected {
} m.RUnlock()
b.mu.RLock()
if !b.connected {
b.mu.RUnlock()
return nil, broker.ErrNotConnected return nil, broker.ErrNotConnected
} }
b.mu.RUnlock() m.RUnlock()
sid, err := id.New() sid, err := id.New()
if err != nil { if err != nil {
@@ -269,7 +252,56 @@ func (b *Broker) fnSubscribe(ctx context.Context, topic string, handler interfac
options := broker.NewSubscribeOptions(opts...) options := broker.NewSubscribeOptions(opts...)
sub := &Subscriber{ 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 broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
return m.funcSubscribe(ctx, topic, handler, opts...)
}
func (m *memoryBroker) fnSubscribe(ctx context.Context, topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
m.RLock()
if !m.connected {
m.RUnlock()
return nil, broker.ErrNotConnected
}
m.RUnlock()
sid, err := id.New()
if err != nil {
return nil, err
}
options := broker.NewSubscribeOptions(opts...)
sub := &memorySubscriber{
exit: make(chan bool, 1), exit: make(chan bool, 1),
id: sid, id: sid,
topic: topic, topic: topic,
@@ -278,64 +310,102 @@ func (b *Broker) fnSubscribe(ctx context.Context, topic string, handler interfac
ctx: ctx, ctx: ctx,
} }
b.mu.Lock() m.Lock()
b.subscribers[topic] = append(b.subscribers[topic], sub) m.subscribers[topic] = append(m.subscribers[topic], sub)
b.mu.Unlock() m.Unlock()
go func() { go func() {
<-sub.exit <-sub.exit
b.mu.Lock() m.Lock()
newSubscribers := make([]*Subscriber, 0, len(b.subscribers)-1) newSubscribers := make([]*memorySubscriber, 0, len(m.subscribers)-1)
for _, sb := range b.subscribers[topic] { for _, sb := range m.subscribers[topic] {
if sb.id == sub.id { if sb.id == sub.id {
continue continue
} }
newSubscribers = append(newSubscribers, sb) newSubscribers = append(newSubscribers, sb)
} }
b.subscribers[topic] = newSubscribers m.subscribers[topic] = newSubscribers
b.mu.Unlock() m.Unlock()
}() }()
return sub, nil return sub, nil
} }
func (b *Broker) String() string { func (m *memoryBroker) String() string {
return "memory" return "memory"
} }
func (b *Broker) Name() string { func (m *memoryBroker) Name() string {
return b.opts.Name return m.opts.Name
} }
func (b *Broker) Live() bool { func (m *memoryBroker) Live() bool {
return true return true
} }
func (b *Broker) Ready() bool { func (m *memoryBroker) Ready() bool {
return true return true
} }
func (b *Broker) Health() bool { func (m *memoryBroker) Health() bool {
return true return true
} }
func (m *Subscriber) Options() broker.SubscribeOptions { func (m *memoryEvent) Topic() string {
return m.opts
}
func (m *Subscriber) Topic() string {
return m.topic return m.topic
} }
func (m *Subscriber) Unsubscribe(ctx context.Context) error { func (m *memoryEvent) Message() *broker.Message {
switch v := m.message.(type) {
case *broker.Message:
return v
case []byte:
msg := &broker.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
}
return nil
}
func (m *memoryEvent) Ack() error {
return nil
}
func (m *memoryEvent) Error() error {
return m.err
}
func (m *memoryEvent) SetError(err error) {
m.err = err
}
func (m *memoryEvent) Context() context.Context {
return m.opts.Context
}
func (m *memorySubscriber) Options() broker.SubscribeOptions {
return m.opts
}
func (m *memorySubscriber) Topic() string {
return m.topic
}
func (m *memorySubscriber) Unsubscribe(ctx context.Context) error {
m.exit <- true m.exit <- true
return nil return nil
} }
// NewBroker return new memory broker // NewBroker return new memory broker
func NewBroker(opts ...broker.Option) broker.Broker { func NewBroker(opts ...broker.Option) broker.Broker {
return &Broker{ return &memoryBroker{
opts: broker.NewOptions(opts...), opts: broker.NewOptions(opts...),
subscribers: make(map[string][]*Subscriber), subscribers: make(map[string][]*memorySubscriber),
} }
} }

View File

@@ -5,23 +5,12 @@ import (
"fmt" "fmt"
"testing" "testing"
"go.uber.org/atomic" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/broker" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/codec"
"go.unistack.org/micro/v4/metadata"
) )
type hldr struct { func TestMemoryBatchBroker(t *testing.T) {
c atomic.Int64 b := NewBroker()
}
func (h *hldr) Handler(m broker.Message) error {
h.c.Add(1)
return nil
}
func TestMemoryBroker(t *testing.T) {
b := NewBroker(broker.Codec("application/octet-stream", codec.NewCodec()))
ctx := context.Background() ctx := context.Background()
if err := b.Init(); err != nil { if err := b.Init(); err != nil {
@@ -33,31 +22,32 @@ func TestMemoryBroker(t *testing.T) {
} }
topic := "test" topic := "test"
count := int64(10) count := 10
h := &hldr{} fn := func(evts broker.Events) error {
return evts.Ack()
}
sub, err := b.Subscribe(ctx, topic, h.Handler) sub, err := b.BatchSubscribe(ctx, topic, fn)
if err != nil { if err != nil {
t.Fatalf("Unexpected error subscribing %v", err) t.Fatalf("Unexpected error subscribing %v", err)
} }
for i := int64(0); i < count; i++ { msgs := make([]*broker.Message, 0, count)
message, err := b.NewMessage(ctx, for i := 0; i < count; i++ {
metadata.Pairs( message := &broker.Message{
"foo", "bar", Header: map[string]string{
"id", fmt.Sprintf("%d", i), metadata.HeaderTopic: topic,
), "foo": "bar",
[]byte(`"hello world"`), "id": fmt.Sprintf("%d", i),
broker.MessageContentType("application/octet-stream"), },
) Body: []byte(`"hello world"`),
if err != nil { }
t.Fatal(err) msgs = append(msgs, message)
} }
if err := b.Publish(ctx, topic, message); err != nil { if err := b.BatchPublish(ctx, msgs); err != nil {
t.Fatalf("Unexpected error publishing %d err: %v", i, err) t.Fatalf("Unexpected error publishing %v", err)
}
} }
if err := sub.Unsubscribe(ctx); err != nil { if err := sub.Unsubscribe(ctx); err != nil {
@@ -67,8 +57,58 @@ func TestMemoryBroker(t *testing.T) {
if err := b.Disconnect(ctx); err != nil { if err := b.Disconnect(ctx); err != nil {
t.Fatalf("Unexpected connect error %v", err) t.Fatalf("Unexpected connect error %v", err)
} }
}
if h.c.Load() != count { func TestMemoryBroker(t *testing.T) {
t.Fatal("invalid messages count received") b := NewBroker()
ctx := context.Background()
if err := b.Init(); err != nil {
t.Fatalf("Unexpected init error %v", err)
}
if err := b.Connect(ctx); err != nil {
t.Fatalf("Unexpected connect error %v", err)
}
topic := "test"
count := 10
fn := func(_ broker.Event) error {
return nil
}
sub, err := b.Subscribe(ctx, topic, fn)
if err != nil {
t.Fatalf("Unexpected error subscribing %v", err)
}
msgs := make([]*broker.Message, 0, count)
for i := 0; i < count; i++ {
message := &broker.Message{
Header: map[string]string{
metadata.HeaderTopic: topic,
"foo": "bar",
"id": fmt.Sprintf("%d", i),
},
Body: []byte(`"hello world"`),
}
msgs = append(msgs, message)
if err := b.Publish(ctx, topic, message); err != nil {
t.Fatalf("Unexpected error publishing %d err: %v", i, err)
}
}
if err := b.BatchPublish(ctx, msgs); err != nil {
t.Fatalf("Unexpected error publishing %v", err)
}
if err := sub.Unsubscribe(ctx); err != nil {
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
}
if err := b.Disconnect(ctx); err != nil {
t.Fatalf("Unexpected connect error %v", err)
} }
} }

View File

@@ -3,37 +3,24 @@ package broker
import ( import (
"context" "context"
"strings" "strings"
"sync"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v4/metadata"
"go.unistack.org/micro/v4/options"
) )
type NoopBroker struct { type NoopBroker struct {
funcPublish FuncPublish funcPublish FuncPublish
funcBatchPublish FuncBatchPublish
funcSubscribe FuncSubscribe funcSubscribe FuncSubscribe
funcBatchSubscribe FuncBatchSubscribe
opts Options opts Options
mu sync.RWMutex
}
func (b *NoopBroker) newCodec(ct string) (codec.Codec, error) {
if idx := strings.IndexRune(ct, ';'); idx >= 0 {
ct = ct[:idx]
}
b.mu.RLock()
c, ok := b.opts.Codecs[ct]
b.mu.RUnlock()
if ok {
return c, nil
}
return nil, codec.ErrUnknownContentType
} }
func NewBroker(opts ...Option) *NoopBroker { func NewBroker(opts ...Option) *NoopBroker {
b := &NoopBroker{opts: NewOptions(opts...)} b := &NoopBroker{opts: NewOptions(opts...)}
b.funcPublish = b.fnPublish b.funcPublish = b.fnPublish
b.funcBatchPublish = b.fnBatchPublish
b.funcSubscribe = b.fnSubscribe b.funcSubscribe = b.fnSubscribe
b.funcBatchSubscribe = b.fnBatchSubscribe
return b return b
} }
@@ -68,14 +55,20 @@ func (b *NoopBroker) Init(opts ...Option) error {
} }
b.funcPublish = b.fnPublish b.funcPublish = b.fnPublish
b.funcBatchPublish = b.fnBatchPublish
b.funcSubscribe = b.fnSubscribe b.funcSubscribe = b.fnSubscribe
b.funcBatchSubscribe = b.fnBatchSubscribe
b.opts.Hooks.EachPrev(func(hook options.Hook) { b.opts.Hooks.EachPrev(func(hook options.Hook) {
switch h := hook.(type) { switch h := hook.(type) {
case HookPublish: case HookPublish:
b.funcPublish = h(b.funcPublish) b.funcPublish = h(b.funcPublish)
case HookBatchPublish:
b.funcBatchPublish = h(b.funcBatchPublish)
case HookSubscribe: case HookSubscribe:
b.funcSubscribe = h(b.funcSubscribe) b.funcSubscribe = h(b.funcSubscribe)
case HookBatchSubscribe:
b.funcBatchSubscribe = h(b.funcBatchSubscribe)
} }
}) })
@@ -94,75 +87,43 @@ func (b *NoopBroker) Address() string {
return strings.Join(b.opts.Addrs, ",") return strings.Join(b.opts.Addrs, ",")
} }
type noopMessage struct { func (b *NoopBroker) fnBatchPublish(_ context.Context, _ []*Message, _ ...PublishOption) error {
c codec.Codec
ctx context.Context
body []byte
hdr metadata.Metadata
opts MessageOptions
}
func (m *noopMessage) Ack() error {
return nil return nil
} }
func (m *noopMessage) Body() []byte { func (b *NoopBroker) BatchPublish(ctx context.Context, msgs []*Message, opts ...PublishOption) error {
return m.body return b.funcBatchPublish(ctx, msgs, opts...)
} }
func (m *noopMessage) Header() metadata.Metadata { func (b *NoopBroker) fnPublish(_ context.Context, _ string, _ *Message, _ ...PublishOption) error {
return m.hdr
}
func (m *noopMessage) Context() context.Context {
return m.ctx
}
func (m *noopMessage) Topic() string {
return ""
}
func (m *noopMessage) Unmarshal(dst interface{}, opts ...codec.Option) error {
return m.c.Unmarshal(m.body, dst)
}
func (b *NoopBroker) NewMessage(ctx context.Context, hdr metadata.Metadata, body interface{}, opts ...MessageOption) (Message, error) {
options := NewMessageOptions(opts...)
if options.ContentType == "" {
options.ContentType = b.opts.ContentType
}
m := &noopMessage{ctx: ctx, hdr: hdr, opts: options}
c, err := b.newCodec(m.opts.ContentType)
if err == nil {
m.body, err = c.Marshal(body)
}
if err != nil {
return nil, err
}
return m, nil
}
func (b *NoopBroker) fnPublish(_ context.Context, _ string, _ ...Message) error {
return nil return nil
} }
func (b *NoopBroker) Publish(ctx context.Context, topic string, msg ...Message) error { func (b *NoopBroker) Publish(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
return b.funcPublish(ctx, topic, msg...) return b.funcPublish(ctx, topic, msg, opts...)
} }
type NoopSubscriber struct { type NoopSubscriber struct {
ctx context.Context ctx context.Context
topic string topic string
handler interface{} handler Handler
batchHandler BatchHandler
opts SubscribeOptions opts SubscribeOptions
} }
func (b *NoopBroker) fnSubscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) { func (b *NoopBroker) fnBatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), batchHandler: handler}, nil
}
func (b *NoopBroker) BatchSubscribe(ctx context.Context, topic string, handler BatchHandler, opts ...SubscribeOption) (Subscriber, error) {
return b.funcBatchSubscribe(ctx, topic, handler, opts...)
}
func (b *NoopBroker) fnSubscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), handler: handler}, nil return &NoopSubscriber{ctx: ctx, topic: topic, opts: NewSubscribeOptions(opts...), handler: handler}, nil
} }
func (b *NoopBroker) Subscribe(ctx context.Context, topic string, handler interface{}, opts ...SubscribeOption) (Subscriber, error) { func (b *NoopBroker) Subscribe(ctx context.Context, topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
return b.funcSubscribe(ctx, topic, handler, opts...) return b.funcSubscribe(ctx, topic, handler, opts...)
} }

View File

@@ -10,9 +10,9 @@ type testHook struct {
} }
func (t *testHook) Publish1(fn FuncPublish) FuncPublish { func (t *testHook) Publish1(fn FuncPublish) FuncPublish {
return func(ctx context.Context, topic string, messages ...Message) error { return func(ctx context.Context, topic string, msg *Message, opts ...PublishOption) error {
t.f = true t.f = true
return fn(ctx, topic, messages...) return fn(ctx, topic, msg, opts...)
} }
} }

View File

@@ -5,13 +5,13 @@ import (
"crypto/tls" "crypto/tls"
"time" "time"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/options" "go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v4/sync" "go.unistack.org/micro/v3/sync"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Options struct // Options struct
@@ -23,8 +23,8 @@ type Options struct {
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 codecs for marshal/unmarshal based on content-type // 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
@@ -37,6 +37,11 @@ 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 Handler
// BatchErrorHandler used when broker can't unmashal incoming messages
BatchErrorHandler BatchHandler
// Addrs holds the broker address // Addrs holds the broker address
Addrs []string Addrs []string
// Hooks can be run before broker Publish/BatchPublish and // Hooks can be run before broker Publish/BatchPublish and
@@ -45,9 +50,6 @@ type Options struct {
// GracefulTimeout contains time to wait to finish in flight requests // GracefulTimeout contains time to wait to finish in flight requests
GracefulTimeout time.Duration GracefulTimeout time.Duration
// ContentType will be used if no content-type set when creating message
ContentType string
} }
// NewOptions create new Options // NewOptions create new Options
@@ -57,22 +59,16 @@ 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,
GracefulTimeout: DefaultGracefulTimeout, GracefulTimeout: DefaultGracefulTimeout,
ContentType: DefaultContentType,
} }
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
} }
return options return options
} }
// DefaultContentType is the default content-type if not specified
var DefaultContentType = ""
// Context sets the context option // Context sets the context option
func Context(ctx context.Context) Option { func Context(ctx context.Context) Option {
return func(o *Options) { return func(o *Options) {
@@ -80,27 +76,17 @@ func Context(ctx context.Context) Option {
} }
} }
// ContentType used by default if not specified // PublishOptions struct
func ContentType(ct string) Option { type PublishOptions struct {
return func(o *Options) { // Context holds external options
o.ContentType = ct
}
}
// MessageOptions struct
type MessageOptions struct {
// ContentType for message body
ContentType string
// BodyOnly flag says the message contains raw body bytes and don't need
// codec Marshal method
BodyOnly bool
// Context holds custom options
Context context.Context Context context.Context
// BodyOnly flag says the message contains raw body bytes
BodyOnly bool
} }
// NewMessageOptions creates MessageOptions struct // NewPublishOptions creates PublishOptions struct
func NewMessageOptions(opts ...MessageOption) MessageOptions { func NewPublishOptions(opts ...PublishOption) PublishOptions {
options := MessageOptions{ options := PublishOptions{
Context: context.Background(), Context: context.Background(),
} }
for _, o := range opts { for _, o := range opts {
@@ -113,6 +99,10 @@ func NewMessageOptions(opts ...MessageOption) MessageOptions {
type SubscribeOptions struct { 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 Handler
// BatchErrorHandler used when broker can't unmashal incoming messages
BatchErrorHandler BatchHandler
// Group holds consumer group // Group holds consumer group
Group string 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
@@ -128,23 +118,23 @@ type SubscribeOptions struct {
// Option func // Option func
type Option func(*Options) type Option func(*Options)
// MessageOption func // PublishOption func
type MessageOption func(*MessageOptions) type PublishOption func(*PublishOptions)
// MessageContentType sets message content-type that used to Marshal // PublishBodyOnly publish only body of the message
func MessageContentType(ct string) MessageOption { func PublishBodyOnly(b bool) PublishOption {
return func(o *MessageOptions) { return func(o *PublishOptions) {
o.ContentType = ct
}
}
// MessageBodyOnly publish only body of the message
func MessageBodyOnly(b bool) MessageOption {
return func(o *MessageOptions) {
o.BodyOnly = b o.BodyOnly = b
} }
} }
// PublishContext sets the context
func PublishContext(ctx context.Context) PublishOption {
return func(o *PublishOptions) {
o.Context = ctx
}
}
// Addrs sets the host addresses to be used by the broker // Addrs sets the host addresses to be used by the broker
func Addrs(addrs ...string) Option { func Addrs(addrs ...string) Option {
return func(o *Options) { return func(o *Options) {
@@ -152,10 +142,51 @@ func Addrs(addrs ...string) Option {
} }
} }
// Codec sets the codec used for encoding/decoding messages // Codec sets the codec used for encoding/decoding used where
func Codec(ct string, c codec.Codec) Option { // a broker does not support headers
func Codec(c codec.Codec) Option {
return func(o *Options) { return func(o *Options) {
o.Codecs[ct] = c o.Codec = c
}
}
// ErrorHandler will catch all broker errors that cant be handled
// in normal way, for example Codec errors
func ErrorHandler(h Handler) Option {
return func(o *Options) {
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
// in normal way, for example Codec errors
func SubscribeErrorHandler(h Handler) SubscribeOption {
return func(o *SubscribeOptions) {
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
} }
} }
@@ -222,6 +253,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 {

View File

@@ -1,14 +0,0 @@
package broker
// IsValidHandler func signature
func IsValidHandler(sub interface{}) error {
switch sub.(type) {
default:
return ErrInvalidHandler
case func(Message) error:
break
case func([]Message) error:
break
}
return nil
}

View File

@@ -5,7 +5,7 @@ import (
"math" "math"
"time" "time"
"go.unistack.org/micro/v4/util/backoff" "go.unistack.org/micro/v3/util/backoff"
) )
// BackoffFunc is the backoff call func // BackoffFunc is the backoff call func

View File

@@ -5,8 +5,8 @@ import (
"context" "context"
"time" "time"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
) )
var ( var (
@@ -29,14 +29,18 @@ var (
) )
// Client is the interface used to make requests to services. // Client is the interface used to make requests to services.
// It supports Request/Response via Transport and Publishing via the Broker.
// It also supports bidirectional streaming of requests. // It also supports bidirectional streaming of requests.
type Client interface { 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
} }
@@ -45,8 +49,20 @@ type (
HookCall func(next FuncCall) FuncCall HookCall func(next FuncCall) FuncCall
FuncStream func(ctx context.Context, req Request, opts ...CallOption) (Stream, error) FuncStream func(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
HookStream func(next FuncStream) FuncStream HookStream func(next FuncStream) FuncStream
FuncPublish func(ctx context.Context, msg Message, opts ...PublishOption) error
HookPublish func(next FuncPublish) FuncPublish
FuncBatchPublish func(ctx context.Context, msg []Message, opts ...PublishOption) error
HookBatchPublish func(next FuncBatchPublish) FuncBatchPublish
) )
// 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
@@ -105,5 +121,11 @@ type Option func(*Options)
// CallOption used by Call or Stream // CallOption used by Call or Stream
type CallOption func(*CallOptions) type CallOption func(*CallOptions)
// PublishOption used by Publish
type PublishOption func(*PublishOptions)
// MessageOption used by NewMessage
type MessageOption func(*MessageOptions)
// RequestOption used by NewRequest // RequestOption used by NewRequest
type RequestOption func(*RequestOptions) type RequestOption func(*RequestOptions)

View File

@@ -32,6 +32,16 @@ func NewContext(ctx context.Context, c Client) context.Context {
return context.WithValue(ctx, clientKey{}, c) return context.WithValue(ctx, clientKey{}, c)
} }
// SetPublishOption returns a function to setup a context with given value
func SetPublishOption(k, v interface{}) PublishOption {
return func(o *PublishOptions) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}
// SetCallOption returns a function to setup a context with given value // SetCallOption returns a function to setup a context with given value
func SetCallOption(k, v interface{}) CallOption { func SetCallOption(k, v interface{}) CallOption {
return func(o *CallOptions) { return func(o *CallOptions) {

View File

@@ -39,6 +39,17 @@ func TestNewNilContext(t *testing.T) {
} }
} }
func TestSetPublishOption(t *testing.T) {
type key struct{}
o := SetPublishOption(key{}, "test")
opts := &PublishOptions{}
o(opts)
if v, ok := opts.Context.Value(key{}).(string); !ok || v == "" {
t.Fatal("SetPublishOption not works")
}
}
func TestSetCallOption(t *testing.T) { func TestSetCallOption(t *testing.T) {
type key struct{} type key struct{}
o := SetCallOption(key{}, "test") o := SetCallOption(key{}, "test")

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"sort" "sort"
"go.unistack.org/micro/v4/errors" "go.unistack.org/micro/v3/errors"
"go.unistack.org/micro/v4/router" "go.unistack.org/micro/v3/router"
) )
// LookupFunc is used to lookup routes for a service // LookupFunc is used to lookup routes for a service

View File

@@ -3,16 +3,18 @@ package client
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"strconv" "strconv"
"time" "time"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/errors" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/errors"
"go.unistack.org/micro/v4/options" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/selector" "go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v4/semconv" "go.unistack.org/micro/v3/selector"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/semconv"
"go.unistack.org/micro/v3/tracer"
) )
// DefaultCodecs will be used to encode/decode data // DefaultCodecs will be used to encode/decode data
@@ -21,11 +23,19 @@ var DefaultCodecs = map[string]codec.Codec{
} }
type noopClient struct { type noopClient struct {
funcPublish FuncPublish
funcBatchPublish FuncBatchPublish
funcCall FuncCall funcCall FuncCall
funcStream FuncStream funcStream FuncStream
opts Options opts Options
} }
type noopMessage struct {
topic string
payload interface{}
opts MessageOptions
}
type noopRequest struct { type noopRequest struct {
body interface{} body interface{}
codec codec.Codec codec codec.Codec
@@ -42,6 +52,8 @@ func NewClient(opts ...Option) Client {
n.funcCall = n.fnCall n.funcCall = n.fnCall
n.funcStream = n.fnStream n.funcStream = n.fnStream
n.funcPublish = n.fnPublish
n.funcBatchPublish = n.fnBatchPublish
return n return n
} }
@@ -146,6 +158,32 @@ func (n *noopStream) CloseSend() error {
return n.err return n.err
} }
func (n *noopMessage) Topic() string {
return n.topic
}
func (n *noopMessage) Payload() interface{} {
return n.payload
}
func (n *noopMessage) ContentType() string {
return n.opts.ContentType
}
func (n *noopMessage) Metadata() metadata.Metadata {
return n.opts.Metadata
}
func (n *noopClient) newCodec(contentType string) (codec.Codec, error) {
if cf, ok := n.opts.Codecs[contentType]; ok {
return cf, nil
}
if cf, ok := DefaultCodecs[contentType]; ok {
return cf, nil
}
return nil, codec.ErrUnknownContentType
}
func (n *noopClient) Init(opts ...Option) error { func (n *noopClient) Init(opts ...Option) error {
for _, o := range opts { for _, o := range opts {
o(&n.opts) o(&n.opts)
@@ -153,6 +191,8 @@ func (n *noopClient) Init(opts ...Option) error {
n.funcCall = n.fnCall n.funcCall = n.fnCall
n.funcStream = n.fnStream n.funcStream = n.fnStream
n.funcPublish = n.fnPublish
n.funcBatchPublish = n.fnBatchPublish
n.opts.Hooks.EachPrev(func(hook options.Hook) { n.opts.Hooks.EachPrev(func(hook options.Hook) {
switch h := hook.(type) { switch h := hook.(type) {
@@ -160,6 +200,10 @@ func (n *noopClient) Init(opts ...Option) error {
n.funcCall = h(n.funcCall) n.funcCall = h(n.funcCall)
case HookStream: case HookStream:
n.funcStream = h(n.funcStream) n.funcStream = h(n.funcStream)
case HookPublish:
n.funcPublish = h(n.funcPublish)
case HookBatchPublish:
n.funcBatchPublish = h(n.funcBatchPublish)
} }
}) })
@@ -332,6 +376,11 @@ func (n *noopClient) NewRequest(service, endpoint string, _ interface{}, _ ...Re
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) {
ts := time.Now() ts := time.Now()
n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc() n.opts.Meter.Counter(semconv.ClientRequestInflight, "endpoint", req.Endpoint()).Inc()
@@ -500,3 +549,90 @@ func (n *noopClient) fnStream(ctx context.Context, req Request, opts ...CallOpti
func (n *noopClient) stream(ctx context.Context, _ string, _ Request, _ CallOptions) (Stream, error) { func (n *noopClient) stream(ctx context.Context, _ string, _ Request, _ CallOptions) (Stream, error) {
return &noopStream{ctx: ctx}, nil return &noopStream{ctx: ctx}, nil
} }
func (n *noopClient) BatchPublish(ctx context.Context, ps []Message, opts ...PublishOption) error {
return n.funcBatchPublish(ctx, ps, opts...)
}
func (n *noopClient) fnBatchPublish(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.funcPublish(ctx, p, opts...)
}
func (n *noopClient) fnPublish(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))
// get proxy
exchange := ""
if v, ok := os.LookupEnv("MICRO_PROXY"); ok {
exchange = v
}
// get the exchange
if len(options.Exchange) > 0 {
exchange = options.Exchange
}
omd, ok := metadata.FromOutgoingContext(ctx)
if !ok {
omd = metadata.New(0)
}
for _, p := range ps {
md := metadata.Copy(omd)
topic := p.Topic()
if len(exchange) > 0 {
topic = exchange
}
md[metadata.HeaderTopic] = topic
iter := p.Metadata().Iterator()
var k, v string
for iter.Next(&k, &v) {
md.Set(k, v)
}
md[metadata.HeaderContentType] = p.ContentType()
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", "%s", err)
}
// set the body
b, err := cf.Marshal(p.Payload())
if err != nil {
return errors.InternalServerError("go.micro.client", "%s", err)
}
body = b
}
msgs = append(msgs, &broker.Message{Header: md, Body: body})
}
if len(msgs) == 1 {
return n.opts.Broker.Publish(ctx, msgs[0].Header[metadata.HeaderTopic], msgs[0],
broker.PublishContext(options.Context),
broker.PublishBodyOnly(options.BodyOnly),
)
}
return n.opts.Broker.BatchPublish(ctx, msgs,
broker.PublishContext(options.Context),
broker.PublishBodyOnly(options.BodyOnly),
)
}

35
client/noop_test.go Normal file
View File

@@ -0,0 +1,35 @@
package client
import (
"context"
"testing"
)
type testHook struct {
f bool
}
func (t *testHook) Publish(fn FuncPublish) FuncPublish {
return func(ctx context.Context, msg Message, opts ...PublishOption) error {
t.f = true
return fn(ctx, msg, opts...)
}
}
func TestNoopHook(t *testing.T) {
h := &testHook{}
c := NewClient(Hooks(HookPublish(h.Publish)))
if err := c.Init(); err != nil {
t.Fatal(err)
}
if err := c.Publish(context.TODO(), c.NewMessage("", nil, MessageContentType("application/octet-stream"))); err != nil {
t.Fatal(err)
}
if !h.f {
t.Fatal("hook not works")
}
}

View File

@@ -6,17 +6,17 @@ import (
"net" "net"
"time" "time"
"go.unistack.org/micro/v4/broker" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/options" "go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v4/router" "go.unistack.org/micro/v3/router"
"go.unistack.org/micro/v4/selector" "go.unistack.org/micro/v3/selector"
"go.unistack.org/micro/v4/selector/random" "go.unistack.org/micro/v3/selector/random"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Options holds client options // Options holds client options
@@ -137,6 +137,43 @@ func Context(ctx context.Context) Option {
} }
} }
// NewPublishOptions create new PublishOptions struct from option
func NewPublishOptions(opts ...PublishOption) PublishOptions {
options := PublishOptions{}
for _, o := range opts {
o(&options)
}
return options
}
// PublishOptions holds publish options
type PublishOptions struct {
// Context used for external options
Context context.Context
// Exchange topic exchange name
Exchange string
// BodyOnly will publish only message body
BodyOnly bool
}
// NewMessageOptions creates message options struct
func NewMessageOptions(opts ...MessageOption) MessageOptions {
options := MessageOptions{Metadata: metadata.New(1)}
for _, o := range opts {
o(&options)
}
return options
}
// MessageOptions holds client message options
type MessageOptions struct {
// Metadata additional metadata
Metadata metadata.Metadata
// ContentType specify content-type of message
// deprecated
ContentType string
}
// NewRequestOptions creates new RequestOptions struct // NewRequestOptions creates new RequestOptions struct
func NewRequestOptions(opts ...RequestOption) RequestOptions { func NewRequestOptions(opts ...RequestOption) RequestOptions {
options := RequestOptions{} options := RequestOptions{}
@@ -337,6 +374,43 @@ func DialTimeout(d time.Duration) Option {
} }
} }
// WithExchange sets the exchange to route a message through
// Deprecated
func WithExchange(e string) PublishOption {
return func(o *PublishOptions) {
o.Exchange = e
}
}
// PublishExchange sets the exchange to route a message through
func PublishExchange(e string) PublishOption {
return func(o *PublishOptions) {
o.Exchange = e
}
}
// WithBodyOnly publish only message body
// DERECATED
func WithBodyOnly(b bool) PublishOption {
return func(o *PublishOptions) {
o.BodyOnly = b
}
}
// PublishBodyOnly publish only message body
func PublishBodyOnly(b bool) PublishOption {
return func(o *PublishOptions) {
o.BodyOnly = b
}
}
// PublishContext sets the context in publish options
func PublishContext(ctx context.Context) PublishOption {
return func(o *PublishOptions) {
o.Context = ctx
}
}
// WithContextDialer pass ContextDialer to client call // WithContextDialer pass ContextDialer to client call
func WithContextDialer(fn func(context.Context, string) (net.Conn, error)) CallOption { func WithContextDialer(fn func(context.Context, string) (net.Conn, error)) CallOption {
return func(o *CallOptions) { return func(o *CallOptions) {
@@ -448,6 +522,30 @@ func WithSelectOptions(sops ...selector.SelectOption) CallOption {
} }
} }
// WithMessageContentType sets the message content type
// Deprecated
func WithMessageContentType(ct string) MessageOption {
return func(o *MessageOptions) {
o.Metadata.Set(metadata.HeaderContentType, ct)
o.ContentType = ct
}
}
// MessageContentType sets the message content type
func MessageContentType(ct string) MessageOption {
return func(o *MessageOptions) {
o.Metadata.Set(metadata.HeaderContentType, ct)
o.ContentType = ct
}
}
// MessageMetadata sets the message metadata
func MessageMetadata(k, v string) MessageOption {
return func(o *MessageOptions) {
o.Metadata.Set(k, v)
}
}
// StreamingRequest specifies that request is streaming // StreamingRequest specifies that request is streaming
func StreamingRequest(b bool) RequestOption { func StreamingRequest(b bool) RequestOption {
return func(o *RequestOptions) { return func(o *RequestOptions) {

View File

@@ -3,7 +3,7 @@ package client
import ( import (
"context" "context"
"go.unistack.org/micro/v4/errors" "go.unistack.org/micro/v3/errors"
) )
// RetryFunc that returning either false or a non-nil error will result in the call not being retried // RetryFunc that returning either false or a non-nil error will result in the call not being retried

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"go.unistack.org/micro/v4/errors" "go.unistack.org/micro/v3/errors"
) )
func TestRetryAlways(t *testing.T) { func TestRetryAlways(t *testing.T) {

View File

@@ -1,7 +1,7 @@
package client package client
import ( import (
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
) )
type testRequest struct { type testRequest struct {

View File

@@ -3,7 +3,7 @@ package cluster
import ( import (
"context" "context"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
) )
// Message sent to member in cluster // Message sent to member in cluster

531
cluster/sql/cluster.go Normal file
View File

@@ -0,0 +1,531 @@
package sql
import (
"context"
"database/sql"
"errors"
"fmt"
"math"
"reflect"
"time"
"unsafe"
"golang.yandex/hasql/v2"
)
var errNoAliveNodes = errors.New("no alive nodes")
func newSQLRowError() *sql.Row {
row := &sql.Row{}
t := reflect.TypeOf(row).Elem()
field, _ := t.FieldByName("err")
rowPtr := unsafe.Pointer(row)
errFieldPtr := unsafe.Pointer(uintptr(rowPtr) + field.Offset)
errPtr := (*error)(errFieldPtr)
*errPtr = errNoAliveNodes
return row
}
type ClusterQuerier interface {
Querier
WaitForNodes(ctx context.Context, criterion ...hasql.NodeStateCriterion) error
}
type Querier interface {
// Basic connection methods
PingContext(ctx context.Context) error
Close() error
// Query methods with context
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
// Prepared statements with context
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
// Transaction management with context
BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
// Connection pool management
SetConnMaxLifetime(d time.Duration)
SetConnMaxIdleTime(d time.Duration)
SetMaxOpenConns(n int)
SetMaxIdleConns(n int)
Stats() sql.DBStats
Conn(ctx context.Context) (*sql.Conn, error)
}
var (
ErrClusterChecker = errors.New("cluster node checker required")
ErrClusterDiscoverer = errors.New("cluster node discoverer required")
ErrClusterPicker = errors.New("cluster node picker required")
)
type Cluster struct {
hasql *hasql.Cluster[Querier]
options ClusterOptions
}
// NewCluster returns Querier that provides cluster of nodes
func NewCluster[T Querier](opts ...ClusterOption) (ClusterQuerier, error) {
options := ClusterOptions{Context: context.Background()}
for _, opt := range opts {
opt(&options)
}
if options.NodeChecker == nil {
return nil, ErrClusterChecker
}
if options.NodeDiscoverer == nil {
return nil, ErrClusterDiscoverer
}
if options.NodePicker == nil {
return nil, ErrClusterPicker
}
if options.Retries < 1 {
options.Retries = 1
}
if options.NodeStateCriterion == 0 {
options.NodeStateCriterion = hasql.Primary
}
options.Options = append(options.Options, hasql.WithNodePicker(options.NodePicker))
if p, ok := options.NodePicker.(*CustomPicker[Querier]); ok {
p.opts.Priority = options.NodePriority
}
c, err := hasql.NewCluster(
options.NodeDiscoverer,
options.NodeChecker,
options.Options...,
)
if err != nil {
return nil, err
}
return &Cluster{hasql: c, options: options}, nil
}
// compile time guard
var _ hasql.NodePicker[Querier] = (*CustomPicker[Querier])(nil)
type nodeStateCriterionKey struct{}
// NodeStateCriterion inject hasql.NodeStateCriterion to context
func NodeStateCriterion(ctx context.Context, c hasql.NodeStateCriterion) context.Context {
return context.WithValue(ctx, nodeStateCriterionKey{}, c)
}
// CustomPickerOptions holds options to pick nodes
type CustomPickerOptions struct {
MaxLag int
Priority map[string]int32
Retries int
}
// CustomPickerOption func apply option to CustomPickerOptions
type CustomPickerOption func(*CustomPickerOptions)
// CustomPickerMaxLag specifies max lag for which node can be used
func CustomPickerMaxLag(n int) CustomPickerOption {
return func(o *CustomPickerOptions) {
o.MaxLag = n
}
}
// NewCustomPicker creates new node picker
func NewCustomPicker[T Querier](opts ...CustomPickerOption) *CustomPicker[Querier] {
options := CustomPickerOptions{}
for _, o := range opts {
o(&options)
}
return &CustomPicker[Querier]{opts: options}
}
// CustomPicker holds node picker options
type CustomPicker[T Querier] struct {
opts CustomPickerOptions
}
// PickNode used to return specific node
func (p *CustomPicker[T]) PickNode(cnodes []hasql.CheckedNode[T]) hasql.CheckedNode[T] {
for _, n := range cnodes {
fmt.Printf("node %s\n", n.Node.String())
}
return cnodes[0]
}
func (p *CustomPicker[T]) getPriority(nodeName string) int32 {
if prio, ok := p.opts.Priority[nodeName]; ok {
return prio
}
return math.MaxInt32 // Default to lowest priority
}
// CompareNodes used to sort nodes
func (p *CustomPicker[T]) CompareNodes(a, b hasql.CheckedNode[T]) int {
fmt.Printf("CompareNodes %s %s\n", a.Node.String(), b.Node.String())
// Get replication lag values
aLag := a.Info.(interface{ ReplicationLag() int }).ReplicationLag()
bLag := b.Info.(interface{ ReplicationLag() int }).ReplicationLag()
// First check that lag lower then MaxLag
if aLag > p.opts.MaxLag && bLag > p.opts.MaxLag {
fmt.Printf("CompareNodes aLag > p.opts.MaxLag && bLag > p.opts.MaxLag\n")
return 0 // both are equal
}
// If one node exceeds MaxLag and the other doesn't, prefer the one that doesn't
if aLag > p.opts.MaxLag {
fmt.Printf("CompareNodes aLag > p.opts.MaxLag\n")
return 1 // b is better
}
if bLag > p.opts.MaxLag {
fmt.Printf("CompareNodes bLag > p.opts.MaxLag\n")
return -1 // a is better
}
// Get node priorities
aPrio := p.getPriority(a.Node.String())
bPrio := p.getPriority(b.Node.String())
// if both priority equals
if aPrio == bPrio {
fmt.Printf("CompareNodes aPrio == bPrio\n")
// First compare by replication lag
if aLag < bLag {
fmt.Printf("CompareNodes aLag < bLag\n")
return -1
}
if aLag > bLag {
fmt.Printf("CompareNodes aLag > bLag\n")
return 1
}
// If replication lag is equal, compare by latency
aLatency := a.Info.(interface{ Latency() time.Duration }).Latency()
bLatency := b.Info.(interface{ Latency() time.Duration }).Latency()
if aLatency < bLatency {
return -1
}
if aLatency > bLatency {
return 1
}
// If lag and latency is equal
return 0
}
// If priorities are different, prefer the node with lower priority value
if aPrio < bPrio {
return -1
}
return 1
}
// ClusterOptions contains cluster specific options
type ClusterOptions struct {
NodeChecker hasql.NodeChecker
NodePicker hasql.NodePicker[Querier]
NodeDiscoverer hasql.NodeDiscoverer[Querier]
Options []hasql.ClusterOpt[Querier]
Context context.Context
Retries int
NodePriority map[string]int32
NodeStateCriterion hasql.NodeStateCriterion
}
// ClusterOption apply cluster options to ClusterOptions
type ClusterOption func(*ClusterOptions)
// WithClusterNodeChecker pass hasql.NodeChecker to cluster options
func WithClusterNodeChecker(c hasql.NodeChecker) ClusterOption {
return func(o *ClusterOptions) {
o.NodeChecker = c
}
}
// WithClusterNodePicker pass hasql.NodePicker to cluster options
func WithClusterNodePicker(p hasql.NodePicker[Querier]) ClusterOption {
return func(o *ClusterOptions) {
o.NodePicker = p
}
}
// WithClusterNodeDiscoverer pass hasql.NodeDiscoverer to cluster options
func WithClusterNodeDiscoverer(d hasql.NodeDiscoverer[Querier]) ClusterOption {
return func(o *ClusterOptions) {
o.NodeDiscoverer = d
}
}
// WithRetries retry count on other nodes in case of error
func WithRetries(n int) ClusterOption {
return func(o *ClusterOptions) {
o.Retries = n
}
}
// WithClusterContext pass context.Context to cluster options and used for checks
func WithClusterContext(ctx context.Context) ClusterOption {
return func(o *ClusterOptions) {
o.Context = ctx
}
}
// WithClusterOptions pass hasql.ClusterOpt
func WithClusterOptions(opts ...hasql.ClusterOpt[Querier]) ClusterOption {
return func(o *ClusterOptions) {
o.Options = append(o.Options, opts...)
}
}
// WithClusterNodeStateCriterion pass default hasql.NodeStateCriterion
func WithClusterNodeStateCriterion(c hasql.NodeStateCriterion) ClusterOption {
return func(o *ClusterOptions) {
o.NodeStateCriterion = c
}
}
type ClusterNode struct {
Name string
DB Querier
Priority int32
}
// WithClusterNodes create cluster with static NodeDiscoverer
func WithClusterNodes(cns ...ClusterNode) ClusterOption {
return func(o *ClusterOptions) {
nodes := make([]*hasql.Node[Querier], 0, len(cns))
if o.NodePriority == nil {
o.NodePriority = make(map[string]int32, len(cns))
}
for _, cn := range cns {
nodes = append(nodes, hasql.NewNode(cn.Name, cn.DB))
if cn.Priority == 0 {
cn.Priority = math.MaxInt32
}
o.NodePriority[cn.Name] = cn.Priority
}
o.NodeDiscoverer = hasql.NewStaticNodeDiscoverer(nodes...)
}
}
func (c *Cluster) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) {
var tx *sql.Tx
var err error
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
for ; retries < c.options.Retries; retries++ {
if tx, err = n.DB().BeginTx(ctx, opts); err != nil && retries >= c.options.Retries {
return true
}
}
return false
})
if tx == nil && err == nil {
err = errNoAliveNodes
}
return tx, err
}
func (c *Cluster) Close() error {
return c.hasql.Close()
}
func (c *Cluster) Conn(ctx context.Context) (*sql.Conn, error) {
var conn *sql.Conn
var err error
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
for ; retries < c.options.Retries; retries++ {
if conn, err = n.DB().Conn(ctx); err != nil && retries >= c.options.Retries {
return true
}
}
return false
})
if conn == nil && err == nil {
err = errNoAliveNodes
}
return conn, err
}
func (c *Cluster) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
var res sql.Result
var err error
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
for ; retries < c.options.Retries; retries++ {
if res, err = n.DB().ExecContext(ctx, query, args...); err != nil && retries >= c.options.Retries {
return true
}
}
return false
})
if res == nil && err == nil {
err = errNoAliveNodes
}
return res, err
}
func (c *Cluster) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
var res *sql.Stmt
var err error
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
for ; retries < c.options.Retries; retries++ {
if res, err = n.DB().PrepareContext(ctx, query); err != nil && retries >= c.options.Retries {
return true
}
}
return false
})
if res == nil && err == nil {
err = errNoAliveNodes
}
return res, err
}
func (c *Cluster) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
var res *sql.Rows
var err error
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
for ; retries < c.options.Retries; retries++ {
if res, err = n.DB().QueryContext(ctx, query); err != nil && err != sql.ErrNoRows && retries >= c.options.Retries {
return true
}
}
return false
})
if res == nil && err == nil {
err = errNoAliveNodes
}
return res, err
}
func (c *Cluster) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
var res *sql.Row
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
for ; retries < c.options.Retries; retries++ {
res = n.DB().QueryRowContext(ctx, query, args...)
if res.Err() == nil {
return false
} else if res.Err() != nil && retries >= c.options.Retries {
return false
}
}
return true
})
if res == nil {
res = newSQLRowError()
}
return res
}
func (c *Cluster) PingContext(ctx context.Context) error {
var err error
var ok bool
retries := 0
c.hasql.NodesIter(c.getNodeStateCriterion(ctx))(func(n *hasql.Node[Querier]) bool {
ok = true
for ; retries < c.options.Retries; retries++ {
if err = n.DB().PingContext(ctx); err != nil && retries >= c.options.Retries {
return true
}
}
return false
})
if !ok {
err = errNoAliveNodes
}
return err
}
func (c *Cluster) WaitForNodes(ctx context.Context, criterions ...hasql.NodeStateCriterion) error {
for _, criterion := range criterions {
if _, err := c.hasql.WaitForNode(ctx, criterion); err != nil {
return err
}
}
return nil
}
func (c *Cluster) SetConnMaxLifetime(td time.Duration) {
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
n.DB().SetConnMaxIdleTime(td)
return false
})
}
func (c *Cluster) SetConnMaxIdleTime(td time.Duration) {
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
n.DB().SetConnMaxIdleTime(td)
return false
})
}
func (c *Cluster) SetMaxOpenConns(nc int) {
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
n.DB().SetMaxOpenConns(nc)
return false
})
}
func (c *Cluster) SetMaxIdleConns(nc int) {
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
n.DB().SetMaxIdleConns(nc)
return false
})
}
func (c *Cluster) Stats() sql.DBStats {
s := sql.DBStats{}
c.hasql.NodesIter(hasql.NodeStateCriterion(hasql.Alive))(func(n *hasql.Node[Querier]) bool {
st := n.DB().Stats()
s.Idle += st.Idle
s.InUse += st.InUse
s.MaxIdleClosed += st.MaxIdleClosed
s.MaxIdleTimeClosed += st.MaxIdleTimeClosed
s.MaxOpenConnections += st.MaxOpenConnections
s.OpenConnections += st.OpenConnections
s.WaitCount += st.WaitCount
s.WaitDuration += st.WaitDuration
return false
})
return s
}
func (c *Cluster) getNodeStateCriterion(ctx context.Context) hasql.NodeStateCriterion {
if v, ok := ctx.Value(nodeStateCriterionKey{}).(hasql.NodeStateCriterion); ok {
return v
}
return c.options.NodeStateCriterion
}

171
cluster/sql/cluster_test.go Normal file
View File

@@ -0,0 +1,171 @@
package sql
import (
"context"
"fmt"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"golang.yandex/hasql/v2"
)
func TestNewCluster(t *testing.T) {
dbMaster, dbMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
if err != nil {
t.Fatal(err)
}
defer dbMaster.Close()
dbMasterMock.MatchExpectationsInOrder(false)
dbMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
sqlmock.NewRowsWithColumnDefinition(
sqlmock.NewColumn("role").OfType("int8", 0),
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
AddRow(1, 0)).
RowsWillBeClosed().
WithoutArgs()
dbMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
sqlmock.NewRows([]string{"name"}).
AddRow("master-dc1"))
dbDRMaster, dbDRMasterMock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
if err != nil {
t.Fatal(err)
}
defer dbDRMaster.Close()
dbDRMasterMock.MatchExpectationsInOrder(false)
dbDRMasterMock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
sqlmock.NewRowsWithColumnDefinition(
sqlmock.NewColumn("role").OfType("int8", 0),
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
AddRow(2, 40)).
RowsWillBeClosed().
WithoutArgs()
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
sqlmock.NewRows([]string{"name"}).
AddRow("drmaster1-dc2"))
dbDRMasterMock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
sqlmock.NewRows([]string{"name"}).
AddRow("drmaster"))
dbSlaveDC1, dbSlaveDC1Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
if err != nil {
t.Fatal(err)
}
defer dbSlaveDC1.Close()
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
dbSlaveDC1Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
sqlmock.NewRowsWithColumnDefinition(
sqlmock.NewColumn("role").OfType("int8", 0),
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
AddRow(2, 50)).
RowsWillBeClosed().
WithoutArgs()
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
sqlmock.NewRows([]string{"name"}).
AddRow("slave-dc1"))
dbSlaveDC2, dbSlaveDC2Mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
if err != nil {
t.Fatal(err)
}
defer dbSlaveDC2.Close()
dbSlaveDC1Mock.MatchExpectationsInOrder(false)
dbSlaveDC2Mock.ExpectQuery(`.*pg_is_in_recovery.*`).WillReturnRows(
sqlmock.NewRowsWithColumnDefinition(
sqlmock.NewColumn("role").OfType("int8", 0),
sqlmock.NewColumn("replication_lag").OfType("int8", 0)).
AddRow(2, 50)).
RowsWillBeClosed().
WithoutArgs()
dbSlaveDC2Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
sqlmock.NewRows([]string{"name"}).
AddRow("slave-dc1"))
tctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
c, err := NewCluster[Querier](
WithClusterContext(tctx),
WithClusterNodeChecker(hasql.PostgreSQLChecker),
WithClusterNodePicker(NewCustomPicker[Querier](
CustomPickerMaxLag(100),
)),
WithClusterNodes(
ClusterNode{"slave-dc1", dbSlaveDC1, 1},
ClusterNode{"master-dc1", dbMaster, 1},
ClusterNode{"slave-dc2", dbSlaveDC2, 2},
ClusterNode{"drmaster1-dc2", dbDRMaster, 0},
),
WithClusterOptions(
hasql.WithUpdateInterval[Querier](2*time.Second),
hasql.WithUpdateTimeout[Querier](1*time.Second),
),
)
if err != nil {
t.Fatal(err)
}
defer c.Close()
if err = c.WaitForNodes(tctx, hasql.Primary, hasql.Standby); err != nil {
t.Fatal(err)
}
time.Sleep(500 * time.Millisecond)
node1Name := ""
fmt.Printf("check for Standby\n")
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.Standby), "SELECT node_name as name"); row.Err() != nil {
t.Fatal(row.Err())
} else if err = row.Scan(&node1Name); err != nil {
t.Fatal(err)
} else if "slave-dc1" != node1Name {
t.Fatalf("invalid node name %s != %s", "slave-dc1", node1Name)
}
dbSlaveDC1Mock.ExpectQuery(`SELECT node_name as name`).WillReturnRows(
sqlmock.NewRows([]string{"name"}).
AddRow("slave-dc1"))
node2Name := ""
fmt.Printf("check for PreferStandby\n")
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferStandby), "SELECT node_name as name"); row.Err() != nil {
t.Fatal(row.Err())
} else if err = row.Scan(&node2Name); err != nil {
t.Fatal(err)
} else if "slave-dc1" != node2Name {
t.Fatalf("invalid node name %s != %s", "slave-dc1", node2Name)
}
node3Name := ""
fmt.Printf("check for PreferPrimary\n")
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferPrimary), "SELECT node_name as name"); row.Err() != nil {
t.Fatal(row.Err())
} else if err = row.Scan(&node3Name); err != nil {
t.Fatal(err)
} else if "master-dc1" != node3Name {
t.Fatalf("invalid node name %s != %s", "master-dc1", node3Name)
}
dbSlaveDC1Mock.ExpectQuery(`.*`).WillReturnRows(sqlmock.NewRows([]string{"role"}).RowError(1, fmt.Errorf("row error")))
time.Sleep(2 * time.Second)
fmt.Printf("check for PreferStandby\n")
if row := c.QueryRowContext(NodeStateCriterion(tctx, hasql.PreferStandby), "SELECT node_name as name"); row.Err() == nil {
t.Fatal("must return error")
}
if dbMasterErr := dbMasterMock.ExpectationsWereMet(); dbMasterErr != nil {
t.Error(dbMasterErr)
}
}

View File

@@ -3,6 +3,8 @@ package codec
import ( import (
"errors" "errors"
"gopkg.in/yaml.v3"
) )
var ( var (
@@ -66,10 +68,10 @@ func (m *RawMessage) MarshalYAML() ([]byte, error) {
} }
// UnmarshalYAML sets *m to a copy of data. // UnmarshalYAML sets *m to a copy of data.
func (m *RawMessage) UnmarshalYAML(data []byte) error { func (m *RawMessage) UnmarshalYAML(n *yaml.Node) error {
if m == nil { if m == nil {
return errors.New("RawMessage UnmarshalYAML on nil pointer") return errors.New("RawMessage UnmarshalYAML on nil pointer")
} }
*m = append((*m)[0:0], data...) *m = append((*m)[0:0], []byte(n.Value)...)
return nil return nil
} }

View File

@@ -1,5 +1,7 @@
package codec package codec
import "gopkg.in/yaml.v3"
// Frame gives us the ability to define raw data to send over the pipes // Frame gives us the ability to define raw data to send over the pipes
type Frame struct { type Frame struct {
Data []byte Data []byte
@@ -26,8 +28,8 @@ func (m *Frame) MarshalYAML() ([]byte, error) {
} }
// UnmarshalYAML set frame data // UnmarshalYAML set frame data
func (m *Frame) UnmarshalYAML(data []byte) error { func (m *Frame) UnmarshalYAML(n *yaml.Node) error {
m.Data = append((m.Data)[0:0], data...) m.Data = []byte(n.Value)
return nil return nil
} }

View File

@@ -17,7 +17,7 @@ syntax = "proto3";
package micro.codec; package micro.codec;
option cc_enable_arenas = true; option cc_enable_arenas = true;
option go_package = "go.unistack.org/micro/v4/codec;codec"; option go_package = "go.unistack.org/micro/v3/codec;codec";
option java_multiple_files = true; option java_multiple_files = true;
option java_outer_classname = "MicroCodec"; option java_outer_classname = "MicroCodec";
option java_package = "micro.codec"; option java_package = "micro.codec";

View File

@@ -3,7 +3,7 @@ package codec
import ( import (
"encoding/json" "encoding/json"
codecpb "go.unistack.org/micro-proto/v4/codec" codecpb "go.unistack.org/micro-proto/v3/codec"
) )
type noopCodec struct { type noopCodec struct {

View File

@@ -3,9 +3,9 @@ package codec
import ( import (
"context" "context"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Option func // Option func

View File

@@ -9,10 +9,10 @@ import (
"dario.cat/mergo" "dario.cat/mergo"
"github.com/google/uuid" "github.com/google/uuid"
"go.unistack.org/micro/v4/options" "go.unistack.org/micro/v3/options"
mid "go.unistack.org/micro/v4/util/id" mid "go.unistack.org/micro/v3/util/id"
rutil "go.unistack.org/micro/v4/util/reflect" rutil "go.unistack.org/micro/v3/util/reflect"
mtime "go.unistack.org/micro/v4/util/time" mtime "go.unistack.org/micro/v3/util/time"
) )
type defaultConfig struct { type defaultConfig struct {
@@ -210,7 +210,7 @@ func fillValue(value reflect.Value, val string) error {
return err return err
} }
value.Set(reflect.ValueOf(v)) value.Set(reflect.ValueOf(v))
case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v4/util/time": case value.Type().String() == "time.Duration" && value.Type().PkgPath() == "go.unistack.org/micro/v3/util/time":
v, err := mtime.ParseDuration(val) v, err := mtime.ParseDuration(val)
if err != nil { if err != nil {
return err return err

View File

@@ -7,8 +7,8 @@ import (
"testing" "testing"
"time" "time"
"go.unistack.org/micro/v4/config" "go.unistack.org/micro/v3/config"
mtime "go.unistack.org/micro/v4/util/time" mtime "go.unistack.org/micro/v3/util/time"
) )
type cfg struct { type cfg struct {

View File

@@ -4,11 +4,11 @@ import (
"context" "context"
"time" "time"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/options" "go.unistack.org/micro/v3/options"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Options hold the config options // Options hold the config options

View File

@@ -17,7 +17,7 @@ syntax = "proto3";
package micro.errors; package micro.errors;
option cc_enable_arenas = true; option cc_enable_arenas = true;
option go_package = "go.unistack.org/micro/v4/errors;errors"; option go_package = "go.unistack.org/micro/v3/errors;errors";
option java_multiple_files = true; option java_multiple_files = true;
option java_outer_classname = "MicroErrors"; option java_outer_classname = "MicroErrors";
option java_package = "micro.errors"; option java_package = "micro.errors";

27
event.go Normal file
View File

@@ -0,0 +1,27 @@
package micro
import (
"context"
"go.unistack.org/micro/v3/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...)
}

View File

@@ -15,6 +15,15 @@ func FromContext(ctx context.Context) (Flow, bool) {
return c, ok return c, ok
} }
// MustContext returns Flow from context
func MustContext(ctx context.Context) Flow {
f, ok := FromContext(ctx)
if !ok {
panic("missing flow")
}
return f
}
// NewContext stores Flow to context // NewContext stores Flow to context
func NewContext(ctx context.Context, f Flow) context.Context { func NewContext(ctx context.Context, f Flow) context.Context {
if ctx == nil { if ctx == nil {

View File

@@ -1,5 +1,3 @@
//go:build ignore
package flow package flow
import ( import (

View File

@@ -1,19 +1,17 @@
//go:build ignore
package flow package flow
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath"
"sync" "sync"
"github.com/heimdalr/dag" "github.com/silas/dag"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/store" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/util/id" "go.unistack.org/micro/v3/store"
"go.unistack.org/micro/v3/util/id"
) )
type microFlow struct { type microFlow struct {
@@ -22,7 +20,7 @@ type microFlow struct {
type microWorkflow struct { type microWorkflow struct {
opts Options opts Options
g *dag.DAG g *dag.AcyclicGraph
steps map[string]Step steps map[string]Step
id string id string
status Status status Status
@@ -34,20 +32,20 @@ func (w *microWorkflow) ID() string {
return w.id return w.id
} }
func (w *microWorkflow) Steps() ([][]Step, error) {
return w.getSteps("", false)
}
func (w *microWorkflow) Status() Status { func (w *microWorkflow) Status() Status {
return w.status return w.status
} }
func (w *microWorkflow) AppendSteps(steps ...Step) error { func (w *microWorkflow) AppendSteps(steps ...Step) error {
var err error
w.Lock() w.Lock()
defer w.Unlock()
for _, s := range steps { for _, s := range steps {
w.steps[s.String()] = s w.steps[s.String()] = s
if _, err = w.g.AddVertex(s); err != nil { w.g.Add(s)
return err
}
} }
for _, dst := range steps { for _, dst := range steps {
@@ -56,13 +54,18 @@ func (w *microWorkflow) AppendSteps(steps ...Step) error {
if !ok { if !ok {
return ErrStepNotExists return ErrStepNotExists
} }
if err = w.g.AddEdge(src.String(), dst.String()); err != nil { w.g.Connect(dag.BasicEdge(src, dst))
return err
}
} }
} }
w.g.ReduceTransitively() if err := w.g.Validate(); err != nil {
w.Unlock()
return err
}
w.g.TransitiveReduction()
w.Unlock()
return nil return nil
} }
@@ -71,11 +74,10 @@ func (w *microWorkflow) RemoveSteps(steps ...Step) error {
// TODO: handle case when some step requires or required by removed step // TODO: handle case when some step requires or required by removed step
w.Lock() w.Lock()
defer w.Unlock()
for _, s := range steps { for _, s := range steps {
delete(w.steps, s.String()) delete(w.steps, s.String())
w.g.DeleteVertex(s.String()) w.g.Remove(s)
} }
for _, dst := range steps { for _, dst := range steps {
@@ -84,34 +86,91 @@ func (w *microWorkflow) RemoveSteps(steps ...Step) error {
if !ok { if !ok {
return ErrStepNotExists return ErrStepNotExists
} }
w.g.AddEdge(src.String(), dst.String()) w.g.Connect(dag.BasicEdge(src, dst))
} }
} }
w.g.ReduceTransitively() if err := w.g.Validate(); err != nil {
w.Unlock()
return err
}
w.g.TransitiveReduction()
w.Unlock()
return nil return nil
} }
func (w *microWorkflow) getSteps(start string, reverse bool) ([][]Step, error) {
var steps [][]Step
var root dag.Vertex
var err error
fn := func(n dag.Vertex, idx int) error {
if idx == 0 {
steps = make([][]Step, 1)
steps[0] = make([]Step, 0, 1)
} else if idx >= len(steps) {
tsteps := make([][]Step, idx+1)
copy(tsteps, steps)
steps = tsteps
steps[idx] = make([]Step, 0, 1)
}
steps[idx] = append(steps[idx], n.(Step))
return nil
}
if start != "" {
var ok bool
w.RLock()
root, ok = w.steps[start]
w.RUnlock()
if !ok {
return nil, ErrStepNotExists
}
} else {
root, err = w.g.Root()
if err != nil {
return nil, err
}
}
if reverse {
err = w.g.SortedReverseDepthFirstWalk([]dag.Vertex{root}, fn)
} else {
err = w.g.SortedDepthFirstWalk([]dag.Vertex{root}, fn)
}
if err != nil {
return nil, err
}
return steps, nil
}
func (w *microWorkflow) Abort(ctx context.Context, id string) error { func (w *microWorkflow) Abort(ctx context.Context, id string) error {
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id)) workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id)
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())}) return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusAborted.String())})
} }
func (w *microWorkflow) Suspend(ctx context.Context, id string) error { func (w *microWorkflow) Suspend(ctx context.Context, id string) error {
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id)) workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id)
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())}) return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusSuspend.String())})
} }
func (w *microWorkflow) Resume(ctx context.Context, id string) error { func (w *microWorkflow) Resume(ctx context.Context, id string) error {
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", id)) workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+id)
return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())}) return workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())})
} }
func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) { func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...ExecuteOption) (string, error) {
w.Lock() w.Lock()
if !w.init { if !w.init {
w.g.ReduceTransitively() if err := w.g.Validate(); err != nil {
w.Unlock()
return "", err
}
w.g.TransitiveReduction()
w.init = true w.init = true
} }
w.Unlock() w.Unlock()
@@ -121,11 +180,26 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...Execu
return "", err return "", err
} }
// stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid)) stepStore := store.NewNamespaceStore(w.opts.Store, "steps"+w.opts.Store.Options().Separator+eid)
workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid)) workflowStore := store.NewNamespaceStore(w.opts.Store, "workflows"+w.opts.Store.Options().Separator+eid)
options := NewExecuteOptions(opts...) options := NewExecuteOptions(opts...)
steps, err := w.getSteps(options.Start, options.Reverse)
if err != nil {
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
w.opts.Logger.Error(w.opts.Context, "store write error", werr)
}
return "", err
}
var wg sync.WaitGroup
cherr := make(chan error, 1)
chstatus := make(chan Status, 1)
nctx, cancel := context.WithCancel(ctx)
defer cancel()
nopts := make([]ExecuteOption, 0, len(opts)+5) nopts := make([]ExecuteOption, 0, len(opts)+5)
nopts = append(nopts, nopts = append(nopts,
@@ -135,152 +209,21 @@ func (w *microWorkflow) Execute(ctx context.Context, req *Message, opts ...Execu
ExecuteMeter(w.opts.Meter), ExecuteMeter(w.opts.Meter),
) )
nopts = append(nopts, opts...) nopts = append(nopts, opts...)
done := make(chan struct{})
if werr := workflowStore.Write(ctx, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
w.opts.Logger.Error(ctx, "store error: %v", werr) w.opts.Logger.Error(w.opts.Context, "store write error", werr)
return eid, werr return eid, werr
} }
for idx := range steps {
var startID string for nidx := range steps[idx] {
if options.Start == "" { cstep := steps[idx][nidx]
mp := w.g.GetRoots() if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusPending.String())}); werr != nil {
if len(mp) != 1 { return eid, werr
return eid, ErrStepNotExists
}
for k := range mp {
startID = k
}
} else {
for k, v := range w.g.GetVertices() {
if v == options.Start {
startID = k
} }
} }
} }
if startID == "" {
return eid, ErrStepNotExists
}
if options.Async {
go w.handleWorkflow(startID, nopts...)
return eid, nil
}
return eid, w.handleWorkflow(startID, nopts...)
}
func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) error {
w.RLock()
defer w.RUnlock()
// stepStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("steps", eid))
// workflowStore := store.NewNamespaceStore(w.opts.Store, filepath.Join("workflows", eid))
// Get IDs of all descendant vertices.
flowIDs, errDes := w.g.GetDescendants(startID)
if errDes != nil {
return errDes
}
// inputChannels provides for input channels for each of the descendant vertices (+ the start-vertex).
inputChannels := make(map[string]chan FlowResult, len(flowIDs)+1)
// Iterate vertex IDs and create an input channel for each of them and a single
// output channel for leaves. Note, this "pre-flight" is needed to ensure we
// really have an input channel regardless of how we traverse the tree and spawn
// workers.
leafCount := 0
for id := range flowIDs {
// Get all parents of this vertex.
parents, errPar := w.g.GetParents(id)
if errPar != nil {
return errPar
}
// Create a buffered input channel that has capacity for all parent results.
inputChannels[id] = make(chan FlowResult, len(parents))
if ok, err := w.g.IsLeaf(id); ok && err == nil {
leafCount += 1
}
}
// outputChannel caries the results of leaf vertices.
outputChannel := make(chan FlowResult, leafCount)
// To also process the start vertex and to have its results being passed to its
// children, add it to the vertex IDs. Also add an input channel for the start
// vertex and feed the inputs to this channel.
flowIDs[startID] = struct{}{}
inputChannels[startID] = make(chan FlowResult, len(inputs))
for _, i := range inputs {
inputChannels[startID] <- i
}
wg := sync.WaitGroup{}
// Iterate all vertex IDs (now incl. start vertex) and handle each worker (incl.
// inputs and outputs) in a separate goroutine.
for id := range flowIDs {
// Get all children of this vertex that later need to be notified. Note, we
// collect all children before the goroutine to be able to release the read
// lock as early as possible.
children, errChildren := w.g.GetChildren(id)
if errChildren != nil {
return errChildren
}
// Remember to wait for this goroutine.
wg.Add(1)
go func(id string) {
// Get this vertex's input channel.
// Note, only concurrent read here, which is fine.
c := inputChannels[id]
// Await all parent inputs and stuff them into a slice.
parentCount := cap(c)
parentResults := make([]FlowResult, parentCount)
for i := 0; i < parentCount; i++ {
parentResults[i] = <-c
}
// Execute the worker.
errWorker := callback(w.g, id, parentResults)
if errWorker != nil {
return errWorker
}
// Send this worker's FlowResult onto all children's input channels or, if it is
// a leaf (i.e. no children), send the result onto the output channel.
if len(children) > 0 {
for child := range children {
inputChannels[child] <- flowResult
}
} else {
outputChannel <- flowResult
}
// "Sign off".
wg.Done()
}(id)
}
// Wait for all go routines to finish.
wg.Wait()
// Await all leaf vertex results and stuff them into a slice.
resultCount := cap(outputChannel)
results := make([]FlowResult, resultCount)
for i := 0; i < resultCount; i++ {
results[i] = <-outputChannel
}
/*
go func() { go func() {
for idx := range steps { for idx := range steps {
for nidx := range steps[idx] { for nidx := range steps[idx] {
@@ -294,7 +237,7 @@ func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) er
return return
} }
if w.opts.Logger.V(logger.TraceLevel) { if w.opts.Logger.V(logger.TraceLevel) {
w.opts.Logger.Tracef(nctx, "will be executed %v", steps[idx][nidx]) w.opts.Logger.Trace(nctx, fmt.Sprintf("will be executed %v", steps[idx][nidx]))
} }
cstep := steps[idx][nidx] cstep := steps[idx][nidx]
// nolint: nestif // nolint: nestif
@@ -302,65 +245,65 @@ func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) er
wg.Add(1) wg.Add(1)
go func(step Step) { go func(step Step) {
defer wg.Done() defer wg.Done()
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "req"), req); werr != nil { if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"req", req); werr != nil {
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil { if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
cherr <- werr cherr <- werr
return return
} }
rsp, serr := step.Execute(nctx, req, nopts...) rsp, serr := step.Execute(nctx, req, nopts...)
if serr != nil { if serr != nil {
step.SetStatus(StatusFailure) step.SetStatus(StatusFailure)
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"rsp", serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
} }
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
} }
cherr <- serr cherr <- serr
return return
} }
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "rsp"), rsp); werr != nil { if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"rsp", rsp); werr != nil {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, filepath.Join(step.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil { if werr := stepStore.Write(ctx, step.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
cherr <- werr cherr <- werr
return return
} }
}(cstep) }(cstep)
wg.Wait() wg.Wait()
} else { } else {
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "req"), req); werr != nil { if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"req", req); werr != nil {
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil { if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusRunning.String())}); werr != nil {
cherr <- werr cherr <- werr
return return
} }
rsp, serr := cstep.Execute(nctx, req, nopts...) rsp, serr := cstep.Execute(nctx, req, nopts...)
if serr != nil { if serr != nil {
cstep.SetStatus(StatusFailure) cstep.SetStatus(StatusFailure)
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"rsp", serr); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
} }
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) { if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil && w.opts.Logger.V(logger.ErrorLevel) {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
} }
cherr <- serr cherr <- serr
return return
} }
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "rsp"), rsp); werr != nil { if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"rsp", rsp); werr != nil {
w.opts.Logger.Errorf(ctx, "store write error: %v", werr) w.opts.Logger.Error(ctx, "store write error", werr)
cherr <- werr cherr <- werr
return return
} }
if werr := stepStore.Write(ctx, filepath.Join(cstep.ID(), "status"), &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil { if werr := stepStore.Write(ctx, cstep.ID()+w.opts.Store.Options().Separator+"status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
cherr <- werr cherr <- werr
return return
} }
@@ -374,7 +317,7 @@ func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) er
return eid, nil return eid, nil
} }
logger.Tracef(ctx, "wait for finish or error") logger.DefaultLogger.Trace(ctx, "wait for finish or error")
select { select {
case <-nctx.Done(): case <-nctx.Done():
err = nctx.Err() err = nctx.Err()
@@ -390,19 +333,19 @@ func (w *microWorkflow) handleWorkflow(startID string, opts ...ExecuteOption) er
switch { switch {
case nctx.Err() != nil: case nctx.Err() != nil:
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusAborted.String())}); werr != nil {
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr) w.opts.Logger.Error(w.opts.Context, "store write error", werr)
} }
case err == nil: case err == nil:
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusSuccess.String())}); werr != nil {
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr) w.opts.Logger.Error(w.opts.Context, "store write error", werr)
} }
case err != nil: case err != nil:
if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil { if werr := workflowStore.Write(w.opts.Context, "status", &codec.Frame{Data: []byte(StatusFailure.String())}); werr != nil {
w.opts.Logger.Errorf(w.opts.Context, "store error: %v", werr) w.opts.Logger.Error(w.opts.Context, "store write error", werr)
} }
} }
*/
return err return eid, err
} }
// NewFlow create new flow // NewFlow create new flow
@@ -442,11 +385,11 @@ func (f *microFlow) WorkflowList(ctx context.Context) ([]Workflow, error) {
} }
func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) { func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step) (Workflow, error) {
w := &microWorkflow{opts: f.opts, id: id, g: &dag.DAG{}, steps: make(map[string]Step, len(steps))} w := &microWorkflow{opts: f.opts, id: id, g: &dag.AcyclicGraph{}, steps: make(map[string]Step, len(steps))}
for _, s := range steps { for _, s := range steps {
w.steps[s.String()] = s w.steps[s.String()] = s
w.g.AddVertex(s) w.g.Add(s)
} }
for _, dst := range steps { for _, dst := range steps {
@@ -455,11 +398,14 @@ func (f *microFlow) WorkflowCreate(ctx context.Context, id string, steps ...Step
if !ok { if !ok {
return nil, ErrStepNotExists return nil, ErrStepNotExists
} }
w.g.AddEdge(src.String(), dst.String()) w.g.Connect(dag.BasicEdge(src, dst))
} }
} }
w.g.ReduceTransitively() if err := w.g.Validate(); err != nil {
return nil, err
}
w.g.TransitiveReduction()
w.init = true w.init = true

View File

@@ -1,5 +1,5 @@
// Package flow is an interface used for saga pattern microservice workflow // Package flow is an interface used for saga pattern microservice workflow
package flow // import "go.unistack.org/micro/v4/flow" package flow
import ( import (
"context" "context"
@@ -7,7 +7,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
) )
var ( var (
@@ -125,6 +125,8 @@ type Workflow interface {
AppendSteps(steps ...Step) error AppendSteps(steps ...Step) error
// Status returns workflow status // Status returns workflow status
Status() Status Status() Status
// Steps returns steps slice where parallel steps returned on the same level
Steps() ([][]Step, error)
// Suspend suspends execution // Suspend suspends execution
Suspend(ctx context.Context, id string) error Suspend(ctx context.Context, id string) error
// Resume resumes execution // Resume resumes execution

View File

@@ -4,11 +4,11 @@ import (
"context" "context"
"time" "time"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/store" "go.unistack.org/micro/v3/store"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Option func // Option func
@@ -123,6 +123,8 @@ type ExecuteOptions struct {
Start string Start string
// Timeout for execution // Timeout for execution
Timeout time.Duration Timeout time.Duration
// Reverse execution
Reverse bool
// Async enables async execution // Async enables async execution
Async bool Async bool
} }
@@ -165,6 +167,13 @@ func ExecuteContext(ctx context.Context) ExecuteOption {
} }
} }
// ExecuteReverse says that dag must be run in reverse order
func ExecuteReverse(b bool) ExecuteOption {
return func(o *ExecuteOptions) {
o.Reverse = b
}
}
// ExecuteTimeout pass timeout time.Duration for execution // ExecuteTimeout pass timeout time.Duration for execution
func ExecuteTimeout(td time.Duration) ExecuteOption { func ExecuteTimeout(td time.Duration) ExecuteOption {
return func(o *ExecuteOptions) { return func(o *ExecuteOptions) {

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
) )
func TestFSMStart(t *testing.T) { func TestFSMStart(t *testing.T) {

33
go.mod
View File

@@ -1,35 +1,44 @@
module go.unistack.org/micro/v4 module go.unistack.org/micro/v3
go 1.24 go 1.24.0
require ( require (
dario.cat/mergo v1.0.1 dario.cat/mergo v1.0.1
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/KimMachineGun/automemlimit v0.7.0 github.com/KimMachineGun/automemlimit v0.6.1
github.com/goccy/go-yaml v1.17.1 github.com/ash3in/uuidv8 v1.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/matoous/go-nanoid v1.5.1 github.com/matoous/go-nanoid v1.5.1
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-20220518035006-a7e85ada93c5
github.com/spf13/cast v1.7.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
go.uber.org/atomic v1.11.0
go.uber.org/automaxprocs v1.6.0 go.uber.org/automaxprocs v1.6.0
go.unistack.org/micro-proto/v4 v4.1.0 go.unistack.org/micro-proto/v3 v3.4.1
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
golang.yandex/hasql/v2 v2.1.0 golang.yandex/hasql/v2 v2.1.0
google.golang.org/grpc v1.69.4 google.golang.org/grpc v1.69.2
google.golang.org/protobuf v1.36.3 google.golang.org/protobuf v1.36.1
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/cilium/ebpf v0.16.0 // indirect
github.com/containerd/cgroups/v3 v3.0.4 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/opencontainers/runtime-spec v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/net v0.34.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.29.0 // indirect go.uber.org/goleak v1.3.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

74
go.sum
View File

@@ -2,20 +2,39 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/KimMachineGun/automemlimit v0.7.0 h1:7G06p/dMSf7G8E6oq+f2uOPuVncFyIlDI/pBWK49u88= github.com/KimMachineGun/automemlimit v0.6.1 h1:ILa9j1onAAMadBsyyUJv5cack8Y1WT26yLj/V+ulKp8=
github.com/KimMachineGun/automemlimit v0.7.0/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/KimMachineGun/automemlimit v0.6.1/go.mod h1:T7xYht7B8r6AG/AqFcUdc7fzd2bIdBKmepfP2S1svPY=
github.com/ash3in/uuidv8 v1.2.0 h1:2oogGdtCPwaVtyvPPGin4TfZLtOGE5F+W++E880G6SI=
github.com/ash3in/uuidv8 v1.2.0/go.mod h1:BnU0wJBxnzdEKmVg4xckBkD+VZuecTFTUP3M0dWgyY4=
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
github.com/containerd/cgroups/v3 v3.0.4 h1:2fs7l3P0Qxb1nKWuJNFiwhp2CqiKzho71DQkDrHJIo4=
github.com/containerd/cgroups/v3 v3.0.4/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -26,10 +45,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4= github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4=
github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -38,34 +66,40 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E= github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I= github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.unistack.org/micro-proto/v4 v4.1.0 h1:qPwL2n/oqh9RE3RTTDgt28XK3QzV597VugQPaw9lKUk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.unistack.org/micro-proto/v4 v4.1.0/go.mod h1:ArmK7o+uFvxSY3dbJhKBBX4Pm1rhWdLEFf3LxBrMtec= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= go.unistack.org/micro-proto/v3 v3.4.1/go.mod h1:okx/cnOhzuCX0ggl/vToatbCupi0O44diiiLLsZ93Zo=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.yandex/hasql/v2 v2.1.0 h1:7CaFFWeHoK5TvA+QvZzlKHlIN5sqNpqM8NSrXskZD/k= golang.yandex/hasql/v2 v2.1.0 h1:7CaFFWeHoK5TvA+QvZzlKHlIN5sqNpqM8NSrXskZD/k=
golang.yandex/hasql/v2 v2.1.0/go.mod h1:3Au1AxuJDCTXmS117BpbI6e+70kGWeyLR1qJAH6HdtA= golang.yandex/hasql/v2 v2.1.0/go.mod h1:3Au1AxuJDCTXmS117BpbI6e+70kGWeyLR1qJAH6HdtA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=

View File

@@ -3,115 +3,74 @@ package metadata
import ( import (
"context" "context"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/server" "go.unistack.org/micro/v3/server"
) )
type wrapper struct { var DefaultMetadataKeys = []string{"x-request-id"}
type hook struct {
keys []string keys []string
client.Client
} }
func NewClientWrapper(keys ...string) client.Wrapper { func NewHook(keys ...string) *hook {
return func(c client.Client) client.Client { return &hook{keys: keys}
handler := &wrapper{
Client: c,
keys: keys,
}
return handler
}
} }
func NewClientCallWrapper(keys ...string) client.CallWrapper { func metadataCopy(ctx context.Context, keys []string) context.Context {
return func(fn client.CallFunc) client.CallFunc {
return func(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
if keys == nil { if keys == nil {
return fn(ctx, addr, req, rsp, opts) return ctx
} }
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil { if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
omd, ook := metadata.FromOutgoingContext(ctx) omd, ook := metadata.FromOutgoingContext(ctx)
if !ook || omd == nil { if !ook || omd == nil {
omd = metadata.New(len(imd)) omd = metadata.New(len(keys))
} }
for _, k := range keys { for _, k := range keys {
if v := imd.Get(k); v != nil { if v, ok := imd.Get(k); ok && v != "" {
omd.Set(k, v...) omd.Set(k, v)
} }
} }
if !ook { if !ook {
ctx = metadata.NewOutgoingContext(ctx, omd) ctx = metadata.NewOutgoingContext(ctx, omd)
} }
} }
return fn(ctx, addr, req, rsp, opts) return ctx
} }
func (w *hook) ClientCall(next client.FuncCall) client.FuncCall {
return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
return next(metadataCopy(ctx, w.keys), req, rsp, opts...)
} }
} }
func (w *wrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { func (w *hook) ClientStream(next client.FuncStream) client.FuncStream {
if w.keys == nil { return func(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
return w.Client.Call(ctx, req, rsp, opts...) return next(metadataCopy(ctx, w.keys), req, opts...)
} }
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
omd, ook := metadata.FromOutgoingContext(ctx)
if !ook || omd == nil {
omd = metadata.New(len(imd))
}
for _, k := range w.keys {
if v := imd.Get(k); v != nil {
omd.Set(k, v...)
}
}
if !ook {
ctx = metadata.NewOutgoingContext(ctx, omd)
}
}
return w.Client.Call(ctx, req, rsp, opts...)
} }
func (w *wrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { func (w *hook) ClientPublish(next client.FuncPublish) client.FuncPublish {
if w.keys == nil { return func(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
return w.Client.Stream(ctx, req, opts...) return next(metadataCopy(ctx, w.keys), msg, opts...)
} }
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
omd, ook := metadata.FromOutgoingContext(ctx)
if !ook || omd == nil {
omd = metadata.New(len(imd))
}
for _, k := range w.keys {
if v := imd.Get(k); v != nil {
omd.Set(k, v...)
}
}
if !ook {
ctx = metadata.NewOutgoingContext(ctx, omd)
}
}
return w.Client.Stream(ctx, req, opts...)
} }
func NewServerHandlerWrapper(keys ...string) server.HandlerWrapper { func (w *hook) ClientBatchPublish(next client.FuncBatchPublish) client.FuncBatchPublish {
return func(fn server.HandlerFunc) server.HandlerFunc { return func(ctx context.Context, msgs []client.Message, opts ...client.PublishOption) error {
return next(metadataCopy(ctx, w.keys), msgs, opts...)
}
}
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
return func(ctx context.Context, req server.Request, rsp interface{}) error { return func(ctx context.Context, req server.Request, rsp interface{}) error {
if keys == nil { return next(metadataCopy(ctx, w.keys), req, rsp)
return fn(ctx, req, rsp)
}
if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil {
omd, ook := metadata.FromOutgoingContext(ctx)
if !ook || omd == nil {
omd = metadata.New(len(imd))
}
for _, k := range keys {
if v := imd.Get(k); v != nil {
omd.Set(k, v...)
} }
} }
if !ook {
ctx = metadata.NewOutgoingContext(ctx, omd) func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
} return func(ctx context.Context, msg server.Message) error {
} return next(metadataCopy(ctx, w.keys), msg)
return fn(ctx, req, rsp)
}
} }
} }

View File

@@ -4,13 +4,14 @@ import (
"context" "context"
"fmt" "fmt"
"go.unistack.org/micro/v4/errors" "go.unistack.org/micro/v3/errors"
"go.unistack.org/micro/v4/server" "go.unistack.org/micro/v3/server"
) )
func NewOptions(opts ...Option) Options { func NewOptions(opts ...Option) Options {
options := Options{ options := Options{
ServerHandlerFn: DefaultServerHandlerFn, ServerHandlerFn: DefaultServerHandlerFn,
ServerSubscriberFn: DefaultServerSubscriberFn,
} }
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@@ -20,6 +21,7 @@ func NewOptions(opts ...Option) Options {
type Options struct { type Options struct {
ServerHandlerFn func(context.Context, server.Request, interface{}, error) error ServerHandlerFn func(context.Context, server.Request, interface{}, error) error
ServerSubscriberFn func(context.Context, server.Message, error) error
} }
type Option func(*Options) type Option func(*Options)
@@ -30,9 +32,20 @@ func ServerHandlerFunc(fn func(context.Context, server.Request, interface{}, err
} }
} }
var DefaultServerHandlerFn = func(ctx context.Context, req server.Request, rsp interface{}, err error) error { func ServerSubscriberFunc(fn func(context.Context, server.Message, error) error) Option {
return func(o *Options) {
o.ServerSubscriberFn = fn
}
}
var (
DefaultServerHandlerFn = func(ctx context.Context, req server.Request, rsp interface{}, err error) error {
return errors.BadRequest("", "%v", err) return errors.BadRequest("", "%v", err)
} }
DefaultServerSubscriberFn = func(ctx context.Context, req server.Message, err error) error {
return errors.BadRequest("", "%v", err)
}
)
var Hook = NewHook() var Hook = NewHook()
@@ -61,3 +74,21 @@ func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
return err return err
} }
} }
func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
return func(ctx context.Context, msg server.Message) (err error) {
defer func() {
r := recover()
switch verr := r.(type) {
case nil:
return
case error:
err = w.opts.ServerSubscriberFn(ctx, msg, verr)
default:
err = w.opts.ServerSubscriberFn(ctx, msg, fmt.Errorf("%v", r))
}
}()
err = next(ctx, msg)
return err
}
}

View File

@@ -4,10 +4,10 @@ import (
"context" "context"
"net/textproto" "net/textproto"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/server" "go.unistack.org/micro/v3/server"
"go.unistack.org/micro/v4/util/id" "go.unistack.org/micro/v3/util/id"
) )
type XRequestIDKey struct{} type XRequestIDKey struct{}
@@ -37,9 +37,12 @@ var DefaultMetadataFunc = func(ctx context.Context) (context.Context, error) {
} }
if xid == "" { if xid == "" {
xid = imd.GetJoined(DefaultMetadataKey) var id string
if xid == "" { if id, iok = imd.Get(DefaultMetadataKey); iok && id != "" {
xid = omd.GetJoined(DefaultMetadataKey) xid = id
}
if id, ook = omd.Get(DefaultMetadataKey); ook && id != "" {
xid = id
} }
} }
@@ -72,6 +75,19 @@ func NewHook() *hook {
return &hook{} return &hook{}
} }
func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
return func(ctx context.Context, msg server.Message) error {
var err error
if xid, ok := msg.Header()[DefaultMetadataKey]; ok {
ctx = context.WithValue(ctx, XRequestIDKey{}, xid)
}
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
return err
}
return next(ctx, msg)
}
}
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler { func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
return func(ctx context.Context, req server.Request, rsp interface{}) error { return func(ctx context.Context, req server.Request, rsp interface{}) error {
var err error var err error
@@ -82,6 +98,26 @@ func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
} }
} }
func (w *hook) ClientBatchPublish(next client.FuncBatchPublish) client.FuncBatchPublish {
return func(ctx context.Context, msgs []client.Message, opts ...client.PublishOption) error {
var err error
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
return err
}
return next(ctx, msgs, opts...)
}
}
func (w *hook) ClientPublish(next client.FuncPublish) client.FuncPublish {
return func(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
var err error
if ctx, err = DefaultMetadataFunc(ctx); err != nil {
return err
}
return next(ctx, msg, opts...)
}
}
func (w *hook) ClientCall(next client.FuncCall) client.FuncCall { func (w *hook) ClientCall(next client.FuncCall) client.FuncCall {
return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
var err error var err error

View File

@@ -2,10 +2,9 @@ package requestid
import ( import (
"context" "context"
"slices"
"testing" "testing"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
) )
func TestDefaultMetadataFunc(t *testing.T) { func TestDefaultMetadataFunc(t *testing.T) {
@@ -25,10 +24,10 @@ func TestDefaultMetadataFunc(t *testing.T) {
t.Fatalf("md missing in outgoing context") t.Fatalf("md missing in outgoing context")
} }
iv := imd.Get(DefaultMetadataKey) _, iok := imd.Get(DefaultMetadataKey)
ov := omd.Get(DefaultMetadataKey) _, ook := omd.Get(DefaultMetadataKey)
if !slices.Equal(iv, ov) { if !iok || !ook {
t.Fatalf("missing metadata key value %v != %v", iv, ov) t.Fatalf("missing metadata key value")
} }
} }

View File

@@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"time" "time"
"go.unistack.org/micro/v4/hooks/requestid" "go.unistack.org/micro/v3/hooks/requestid"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
var ( var (

View File

@@ -67,10 +67,9 @@ func main() {
} }
h := getHash(ifaces) h := getHash(ifaces)
b.WriteString(fmt.Sprintf("\tif _, ok := dc.(wrapConn%04d_%s); ok {\n", n, h)) b.WriteString(fmt.Sprintf("\tif _, ok := dc.(wrapConn%04d_%s); ok {\n", n, h))
b.WriteString("\treturn struct {\n") b.WriteString("\t\treturn struct {\n")
b.WriteString("\t\tdriver.Conn\n") b.WriteString(strings.Join(append([]string{"\t\t\tdriver.Conn"}, ifaces...), "\n\t\t\t"))
b.WriteString(fmt.Sprintf("\t\t\t%s", strings.Join(ifaces, "\n\t\t\t"))) b.WriteString("\n\t\t}{")
b.WriteString("\t\t\n}{")
for idx := range ifaces { for idx := range ifaces {
if idx > 0 { if idx > 0 {
b.WriteString(", ") b.WriteString(", ")
@@ -82,10 +81,10 @@ func main() {
} }
} }
b.WriteString(", c}\n") b.WriteString(", c}\n")
b.WriteString("}\n\n") b.WriteString("\t}\n\n")
} }
b.WriteString("return c\n") b.WriteString("\treturn c\n")
b.WriteString("}\n") b.WriteString("}\n\n")
for idx := len(comboConn) - 1; idx >= 0; idx-- { for idx := len(comboConn) - 1; idx >= 0; idx-- {
ifaces := comboConn[idx] ifaces := comboConn[idx]
@@ -113,10 +112,9 @@ func main() {
} }
h := getHash(ifaces) h := getHash(ifaces)
b.WriteString(fmt.Sprintf("\tif _, ok := stmt.(wrapStmt%04d_%s); ok {\n", n, h)) b.WriteString(fmt.Sprintf("\tif _, ok := stmt.(wrapStmt%04d_%s); ok {\n", n, h))
b.WriteString("\treturn struct {\n") b.WriteString("\t\treturn struct {\n")
b.WriteString("\t\tdriver.Stmt\n") b.WriteString(strings.Join(append([]string{"\t\t\tdriver.Stmt"}, ifaces...), "\n\t\t\t"))
b.WriteString(fmt.Sprintf("\t\t\t%s", strings.Join(ifaces, "\n\t\t\t"))) b.WriteString("\n\t\t}{")
b.WriteString("\t\t\n}{")
for idx := range ifaces { for idx := range ifaces {
if idx > 0 { if idx > 0 {
b.WriteString(", ") b.WriteString(", ")
@@ -128,9 +126,9 @@ func main() {
} }
} }
b.WriteString(", c}\n") b.WriteString(", c}\n")
b.WriteString("}\n\n") b.WriteString("\t}\n\n")
} }
b.WriteString("return c\n") b.WriteString("\treturn c\n")
b.WriteString("}\n") b.WriteString("}\n")
for idx := len(comboStmt) - 1; idx >= 0; idx-- { for idx := len(comboStmt) - 1; idx >= 0; idx-- {
@@ -140,15 +138,15 @@ func main() {
continue continue
} }
h := getHash(ifaces) h := getHash(ifaces)
b.WriteString(fmt.Sprintf("// %s\n", strings.Join(ifaces, "|"))) b.WriteString(fmt.Sprintf("\n// %s\n", strings.Join(ifaces, "|")))
b.WriteString(fmt.Sprintf("type wrapStmt%04d_%s interface {\n", n, h)) b.WriteString(fmt.Sprintf("type wrapStmt%04d_%s interface {\n", n, h))
for _, iface := range ifaces { for _, iface := range ifaces {
b.WriteString(fmt.Sprintf("\t%s\n", iface)) b.WriteString(fmt.Sprintf("\t%s\n", iface))
} }
b.WriteString("}\n\n") b.WriteString("}\n")
} }
fmt.Printf("%s\n", b.String()) fmt.Printf("%s", b.String())
} }
// all returns all combinations for a given string array. // all returns all combinations for a given string array.

View File

@@ -5,9 +5,9 @@ import (
"fmt" "fmt"
"time" "time"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
var ( var (

View File

@@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"time" "time"
requestid "go.unistack.org/micro/v4/hooks/requestid" "go.unistack.org/micro/v3/hooks/requestid"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
var ( var (

View File

@@ -5,7 +5,7 @@ import (
"database/sql/driver" "database/sql/driver"
"time" "time"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
var _ driver.Tx = (*wrapperTx)(nil) var _ driver.Tx = (*wrapperTx)(nil)

View File

@@ -3,9 +3,9 @@ package validator
import ( import (
"context" "context"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/errors" "go.unistack.org/micro/v3/errors"
"go.unistack.org/micro/v4/server" "go.unistack.org/micro/v3/server"
) )
var ( var (
@@ -22,17 +22,29 @@ var (
} }
return errors.BadRequest(req.Service(), "%v", err) return errors.BadRequest(req.Service(), "%v", err)
} }
DefaultPublishErrorFunc = func(msg client.Message, err error) error {
return errors.BadRequest(msg.Topic(), "%v", err)
}
DefaultSubscribeErrorFunc = func(msg server.Message, err error) error {
return errors.BadRequest(msg.Topic(), "%v", err)
}
) )
type ( type (
ClientErrorFunc func(client.Request, interface{}, error) error ClientErrorFunc func(client.Request, interface{}, error) error
ServerErrorFunc func(server.Request, interface{}, error) error ServerErrorFunc func(server.Request, interface{}, error) error
PublishErrorFunc func(client.Message, error) error
SubscribeErrorFunc func(server.Message, error) error
) )
// Options struct holds wrapper options // Options struct holds wrapper options
type Options struct { type Options struct {
ClientErrorFn ClientErrorFunc ClientErrorFn ClientErrorFunc
ServerErrorFn ServerErrorFunc ServerErrorFn ServerErrorFunc
PublishErrorFn PublishErrorFunc
SubscribeErrorFn SubscribeErrorFunc
ClientValidateResponse bool ClientValidateResponse bool
ServerValidateResponse bool ServerValidateResponse bool
} }
@@ -64,10 +76,24 @@ func ServerErrorFn(fn ServerErrorFunc) Option {
} }
} }
func PublishErrorFn(fn PublishErrorFunc) Option {
return func(o *Options) {
o.PublishErrorFn = fn
}
}
func SubscribeErrorFn(fn SubscribeErrorFunc) Option {
return func(o *Options) {
o.SubscribeErrorFn = fn
}
}
func NewOptions(opts ...Option) Options { func NewOptions(opts ...Option) Options {
options := Options{ options := Options{
ClientErrorFn: DefaultClientErrorFunc, ClientErrorFn: DefaultClientErrorFunc,
ServerErrorFn: DefaultServerErrorFunc, ServerErrorFn: DefaultServerErrorFunc,
PublishErrorFn: DefaultPublishErrorFunc,
SubscribeErrorFn: DefaultSubscribeErrorFunc,
} }
for _, o := range opts { for _, o := range opts {
o(&options) o(&options)
@@ -115,6 +141,30 @@ func (w *hook) ClientStream(next client.FuncStream) client.FuncStream {
} }
} }
func (w *hook) ClientPublish(next client.FuncPublish) client.FuncPublish {
return func(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
if v, ok := msg.Payload().(validator); ok {
if err := v.Validate(); err != nil {
return w.opts.PublishErrorFn(msg, err)
}
}
return next(ctx, msg, opts...)
}
}
func (w *hook) ClientBatchPublish(next client.FuncBatchPublish) client.FuncBatchPublish {
return func(ctx context.Context, msgs []client.Message, opts ...client.PublishOption) error {
for _, msg := range msgs {
if v, ok := msg.Payload().(validator); ok {
if err := v.Validate(); err != nil {
return w.opts.PublishErrorFn(msg, err)
}
}
}
return next(ctx, msgs, opts...)
}
}
func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler { func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
return func(ctx context.Context, req server.Request, rsp interface{}) error { return func(ctx context.Context, req server.Request, rsp interface{}) error {
if v, ok := req.Body().(validator); ok { if v, ok := req.Body().(validator); ok {
@@ -131,3 +181,14 @@ func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler {
return err return err
} }
} }
func (w *hook) ServerSubscriber(next server.FuncSubHandler) server.FuncSubHandler {
return func(ctx context.Context, msg server.Message) error {
if v, ok := msg.Body().(validator); ok {
if err := v.Validate(); err != nil {
return w.opts.SubscribeErrorFn(msg, err)
}
}
return next(ctx, msg)
}
}

View File

@@ -4,20 +4,18 @@ package logger
type Level int8 type Level int8
const ( const (
// TraceLevel usually used to find bugs, very verbose // TraceLevel level usually used to find bugs, very verbose
TraceLevel Level = iota - 2 TraceLevel Level = iota - 2
// DebugLevel used only when enabled debugging // DebugLevel level used only when enabled debugging
DebugLevel DebugLevel
// InfoLevel used for general info about what's going on inside the application // InfoLevel level used for general info about what's going on inside the application
InfoLevel InfoLevel
// WarnLevel used for non-critical entries // WarnLevel level used for non-critical entries
WarnLevel WarnLevel
// ErrorLevel used for errors that should definitely be noted // ErrorLevel level used for errors that should definitely be noted
ErrorLevel ErrorLevel
// FatalLevel used for critical errors and then calls `os.Exit(1)` // FatalLevel level used for critical errors and then calls `os.Exit(1)`
FatalLevel FatalLevel
// NoneLevel used to disable logging
NoneLevel
) )
// String returns logger level string representation // String returns logger level string representation
@@ -35,8 +33,6 @@ func (l Level) String() string {
return "error" return "error"
case FatalLevel: case FatalLevel:
return "fatal" return "fatal"
case NoneLevel:
return "none"
} }
return "info" return "info"
} }
@@ -62,8 +58,6 @@ func ParseLevel(lvl string) Level {
return ErrorLevel return ErrorLevel
case FatalLevel.String(): case FatalLevel.String():
return FatalLevel return FatalLevel
case NoneLevel.String():
return NoneLevel
} }
return InfoLevel return InfoLevel
} }

View File

@@ -8,7 +8,7 @@ import (
"slices" "slices"
"time" "time"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
) )
// Option func signature // Option func signature

View File

@@ -13,9 +13,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/semconv" "go.unistack.org/micro/v3/semconv"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
const ( const (
@@ -34,7 +34,6 @@ var (
warnValue = slog.StringValue("warn") warnValue = slog.StringValue("warn")
errorValue = slog.StringValue("error") errorValue = slog.StringValue("error")
fatalValue = slog.StringValue("fatal") fatalValue = slog.StringValue("fatal")
noneValue = slog.StringValue("none")
) )
type wrapper struct { type wrapper struct {
@@ -86,8 +85,6 @@ func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr {
a.Value = errorValue a.Value = errorValue
case lvl >= logger.FatalLevel: case lvl >= logger.FatalLevel:
a.Value = fatalValue a.Value = fatalValue
case lvl >= logger.NoneLevel:
a.Value = noneValue
default: default:
a.Value = infoValue a.Value = infoValue
} }
@@ -319,8 +316,6 @@ func loggerToSlogLevel(level logger.Level) slog.Level {
return slog.LevelDebug - 1 return slog.LevelDebug - 1
case logger.FatalLevel: case logger.FatalLevel:
return slog.LevelError + 1 return slog.LevelError + 1
case logger.NoneLevel:
return slog.LevelError + 2
default: default:
return slog.LevelInfo return slog.LevelInfo
} }
@@ -338,8 +333,6 @@ func slogToLoggerLevel(level slog.Level) logger.Level {
return logger.TraceLevel return logger.TraceLevel
case slog.LevelError + 1: case slog.LevelError + 1:
return logger.FatalLevel return logger.FatalLevel
case slog.LevelError + 2:
return logger.NoneLevel
default: default:
return logger.InfoLevel return logger.InfoLevel
} }

View File

@@ -12,9 +12,9 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/util/buffer" "go.unistack.org/micro/v3/util/buffer"
) )
// always first to have proper check // always first to have proper check
@@ -36,24 +36,6 @@ func TestStacktrace(t *testing.T) {
} }
} }
func TestNoneLevel(t *testing.T) {
ctx := context.TODO()
buf := bytes.NewBuffer(nil)
l := NewLogger(logger.WithLevel(logger.NoneLevel), logger.WithOutput(buf),
WithHandlerFunc(slog.NewTextHandler),
logger.WithAddStacktrace(true),
)
if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
t.Fatal(err)
}
l.Error(ctx, "msg1", errors.New("err"))
if buf.Len() != 0 {
t.Fatalf("logger none level not works, buf contains: %s", buf.Bytes())
}
}
func TestDelayedBuffer(t *testing.T) { func TestDelayedBuffer(t *testing.T) {
ctx := context.TODO() ctx := context.TODO()
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
@@ -80,7 +62,7 @@ func TestTime(t *testing.T) {
WithHandlerFunc(slog.NewTextHandler), WithHandlerFunc(slog.NewTextHandler),
logger.WithAddStacktrace(true), logger.WithAddStacktrace(true),
logger.WithTimeFunc(func() time.Time { logger.WithTimeFunc(func() time.Time {
return time.Unix(0, 0).UTC() return time.Unix(0, 0)
}), }),
) )
if err := l.Init(logger.WithFields("key1", "val1")); err != nil { if err := l.Init(logger.WithFields("key1", "val1")); err != nil {
@@ -89,7 +71,8 @@ func TestTime(t *testing.T) {
l.Error(ctx, "msg1", errors.New("err")) l.Error(ctx, "msg1", errors.New("err"))
if !bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T00:00:00.000000000Z`)) { if !bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T03:00:00.000000000+03:00`)) &&
!bytes.Contains(buf.Bytes(), []byte(`timestamp=1970-01-01T00:00:00.000000000Z`)) {
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes()) t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
} }
} }
@@ -423,16 +406,15 @@ func TestLogger(t *testing.T) {
func Test_WithContextAttrFunc(t *testing.T) { func Test_WithContextAttrFunc(t *testing.T) {
loggerContextAttrFuncs := []logger.ContextAttrFunc{ loggerContextAttrFuncs := []logger.ContextAttrFunc{
func(ctx context.Context) []interface{} { func(ctx context.Context) []interface{} {
md, ok := metadata.FromOutgoingContext(ctx) md, ok := metadata.FromIncomingContext(ctx)
if !ok { if !ok {
return nil return nil
} }
attrs := make([]interface{}, 0, 10) attrs := make([]interface{}, 0, 10)
for k, v := range md { for k, v := range md {
key := strings.ToLower(k) switch k {
switch key { case "X-Request-Id", "Phone", "External-Id", "Source-Service", "X-App-Install-Id", "Client-Id", "Client-Ip":
case "x-request-id", "phone", "external-Id", "source-service", "x-app-install-id", "client-id", "client-ip": attrs = append(attrs, strings.ToLower(k), v)
attrs = append(attrs, key, v[0])
} }
} }
return attrs return attrs
@@ -442,7 +424,7 @@ func Test_WithContextAttrFunc(t *testing.T) {
logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, loggerContextAttrFuncs...) logger.DefaultContextAttrFuncs = append(logger.DefaultContextAttrFuncs, loggerContextAttrFuncs...)
ctx := context.TODO() ctx := context.TODO()
ctx = metadata.AppendOutgoingContext(ctx, "X-Request-Id", uuid.New().String(), ctx = metadata.AppendIncomingContext(ctx, "X-Request-Id", uuid.New().String(),
"Source-Service", "Test-System") "Source-Service", "Test-System")
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
@@ -462,9 +444,9 @@ func Test_WithContextAttrFunc(t *testing.T) {
t.Fatalf("logger info, buf %s", buf.Bytes()) t.Fatalf("logger info, buf %s", buf.Bytes())
} }
buf.Reset() buf.Reset()
omd, _ := metadata.FromOutgoingContext(ctx) imd, _ := metadata.FromIncomingContext(ctx)
l.Info(ctx, "test message1") l.Info(ctx, "test message1")
omd.Set("Source-Service", "Test-System2") imd.Set("Source-Service", "Test-System2")
l.Info(ctx, "test message2") l.Info(ctx, "test message2")
// t.Logf("xxx %s", buf.Bytes()) // t.Logf("xxx %s", buf.Bytes())

View File

@@ -8,7 +8,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
) )
const sf = "0-+# " const sf = "0-+# "

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
) )
func TestUnwrap(t *testing.T) { func TestUnwrap(t *testing.T) {

View File

@@ -1,185 +1,32 @@
// Package metadata is a way of defining message headers
package metadata package metadata
import ( import (
"context" "context"
"fmt"
"strings"
) )
// In the metadata package, context and metadata are treated as immutable.
// Deep copies of metadata are made to keep things safe and correct.
// If a user takes a map and changes it across threads, it's their responsibility.
//
// 1. Incoming Context
//
// This context is provided by an external system and populated by the server or broker of the micro framework.
// It should not be modified. The idea is to extract all necessary data from it,
// validate the data, and transfer it into the current context.
// After that, only the current context should be used throughout the code.
//
// 2. Current Context
//
// This is the context used during the execution flow.
// You can add any needed metadata to it and pass it through your code.
//
// 3. Outgoing Context
//
// This context is for sending data to external systems.
// You can add what you need before sending it out.
// But its usually better to build and prepare this context right before making the external call,
// instead of changing it in many places.
//
// Execution Flow:
//
// [External System]
// ↓
// [Incoming Context]
// ↓
// [Extract & Validate Metadata from Incoming Context]
// ↓
// [Prepare Current Context]
// ↓
// [Enrich Current Context]
// ↓
// [Business Logic]
// ↓
// [Prepare Outgoing Context]
// ↓
// [External System Call]
type ( type (
metadataCurrentKey struct{} mdIncomingKey struct{}
metadataIncomingKey struct{} mdOutgoingKey struct{}
metadataOutgoingKey struct{} mdKey struct{}
rawMetadata struct {
md Metadata
added [][]string
}
) )
// NewContext creates a new context with the provided Metadata attached. // FromIncomingContext returns metadata from incoming ctx
// The Metadata must not be modified after calling this function. // returned metadata shoud not be modified or race condition happens
func NewContext(ctx context.Context, md Metadata) context.Context {
return context.WithValue(ctx, metadataCurrentKey{}, rawMetadata{md: md})
}
// NewIncomingContext creates a new context with the provided incoming Metadata attached.
// The Metadata must not be modified after calling this function.
func NewIncomingContext(ctx context.Context, md Metadata) context.Context {
return context.WithValue(ctx, metadataIncomingKey{}, rawMetadata{md: md})
}
// NewOutgoingContext creates a new context with the provided outgoing Metadata attached.
// The Metadata must not be modified after calling this function.
func NewOutgoingContext(ctx context.Context, md Metadata) context.Context {
return context.WithValue(ctx, metadataOutgoingKey{}, rawMetadata{md: md})
}
// AppendContext returns a new context with the provided key-value pairs (kv)
// merged with any existing metadata in the context. For a description of kv,
// please refer to the Pairs documentation.
func AppendContext(ctx context.Context, kv ...string) context.Context {
if len(kv)%2 == 1 {
panic(fmt.Sprintf("metadata: AppendContext got an odd number of input pairs for metadata: %d", len(kv)))
}
md, _ := ctx.Value(metadataCurrentKey{}).(rawMetadata)
added := make([][]string, len(md.added)+1)
copy(added, md.added)
kvCopy := make([]string, 0, len(kv))
for i := 0; i < len(kv); i += 2 {
kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
}
added[len(added)-1] = kvCopy
return context.WithValue(ctx, metadataCurrentKey{}, rawMetadata{md: md.md, added: added})
}
// AppendOutgoingContext returns a new context with the provided key-value pairs (kv)
// merged with any existing metadata in the context. For a description of kv,
// please refer to the Pairs documentation.
func AppendOutgoingContext(ctx context.Context, kv ...string) context.Context {
if len(kv)%2 == 1 {
panic(fmt.Sprintf("metadata: AppendOutgoingContext got an odd number of input pairs for metadata: %d", len(kv)))
}
md, _ := ctx.Value(metadataOutgoingKey{}).(rawMetadata)
added := make([][]string, len(md.added)+1)
copy(added, md.added)
kvCopy := make([]string, 0, len(kv))
for i := 0; i < len(kv); i += 2 {
kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
}
added[len(added)-1] = kvCopy
return context.WithValue(ctx, metadataOutgoingKey{}, rawMetadata{md: md.md, added: added})
}
// FromContext retrieves a deep copy of the metadata from the context and returns it
// with a boolean indicating if it was found.
func FromContext(ctx context.Context) (Metadata, bool) {
raw, ok := ctx.Value(metadataCurrentKey{}).(rawMetadata)
if !ok {
return nil, false
}
metadataSize := len(raw.md)
for i := range raw.added {
metadataSize += len(raw.added[i]) / 2
}
out := make(Metadata, metadataSize)
for k, v := range raw.md {
out[k] = copyOf(v)
}
for _, added := range raw.added {
if len(added)%2 == 1 {
panic(fmt.Sprintf("metadata: FromContext got an odd number of input pairs for metadata: %d", len(added)))
}
for i := 0; i < len(added); i += 2 {
out[added[i]] = append(out[added[i]], added[i+1])
}
}
return out, true
}
// MustContext retrieves a deep copy of the metadata from the context and panics
// if the metadata is not found.
func MustContext(ctx context.Context) Metadata {
md, ok := FromContext(ctx)
if !ok {
panic("missing metadata")
}
return md
}
// FromIncomingContext retrieves a deep copy of the metadata from the context and returns it
// with a boolean indicating if it was found.
func FromIncomingContext(ctx context.Context) (Metadata, bool) { func FromIncomingContext(ctx context.Context) (Metadata, bool) {
raw, ok := ctx.Value(metadataIncomingKey{}).(rawMetadata) if ctx == nil {
if !ok {
return nil, false return nil, false
} }
metadataSize := len(raw.md) md, ok := ctx.Value(mdIncomingKey{}).(*rawMetadata)
for i := range raw.added { if !ok || md.md == nil {
metadataSize += len(raw.added[i]) / 2 return nil, false
}
return md.md, ok
} }
out := make(Metadata, metadataSize) // MustIncomingContext returns metadata from incoming ctx
for k, v := range raw.md { // returned metadata shoud not be modified or race condition happens.
out[k] = copyOf(v) // If metadata not exists panics.
}
for _, added := range raw.added {
if len(added)%2 == 1 {
panic(fmt.Sprintf("metadata: FromIncomingContext got an odd number of input pairs for metadata: %d", len(added)))
}
for i := 0; i < len(added); i += 2 {
out[added[i]] = append(out[added[i]], added[i+1])
}
}
return out, true
}
// MustIncomingContext retrieves a deep copy of the metadata from the context and panics
// if the metadata is not found.
func MustIncomingContext(ctx context.Context) Metadata { func MustIncomingContext(ctx context.Context) Metadata {
md, ok := FromIncomingContext(ctx) md, ok := FromIncomingContext(ctx)
if !ok { if !ok {
@@ -188,37 +35,22 @@ func MustIncomingContext(ctx context.Context) Metadata {
return md return md
} }
// FromOutgoingContext retrieves a deep copy of the metadata from the context and returns it // FromOutgoingContext returns metadata from outgoing ctx
// with a boolean indicating if it was found. // returned metadata shoud not be modified or race condition happens
func FromOutgoingContext(ctx context.Context) (Metadata, bool) { func FromOutgoingContext(ctx context.Context) (Metadata, bool) {
raw, ok := ctx.Value(metadataOutgoingKey{}).(rawMetadata) if ctx == nil {
if !ok {
return nil, false return nil, false
} }
md, ok := ctx.Value(mdOutgoingKey{}).(*rawMetadata)
metadataSize := len(raw.md) if !ok || md.md == nil {
for i := range raw.added { return nil, false
metadataSize += len(raw.added[i]) / 2 }
return md.md, ok
} }
out := make(Metadata, metadataSize) // MustOutgoingContext returns metadata from outgoing ctx
for k, v := range raw.md { // returned metadata shoud not be modified or race condition happens.
out[k] = copyOf(v) // If metadata not exists panics.
}
for _, added := range raw.added {
if len(added)%2 == 1 {
panic(fmt.Sprintf("metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d", len(added)))
}
for i := 0; i < len(added); i += 2 {
out[added[i]] = append(out[added[i]], added[i+1])
}
}
return out, ok
}
// MustOutgoingContext retrieves a deep copy of the metadata from the context and panics
// if the metadata is not found.
func MustOutgoingContext(ctx context.Context) Metadata { func MustOutgoingContext(ctx context.Context) Metadata {
md, ok := FromOutgoingContext(ctx) md, ok := FromOutgoingContext(ctx)
if !ok { if !ok {
@@ -227,68 +59,121 @@ func MustOutgoingContext(ctx context.Context) Metadata {
return md return md
} }
// ValueFromCurrentContext retrieves a deep copy of the metadata for the given key // FromContext returns metadata from the given context
// from the context, performing a case-insensitive search if needed. Returns nil if not found. // returned metadata shoud not be modified or race condition happens
func ValueFromCurrentContext(ctx context.Context, key string) []string { func FromContext(ctx context.Context) (Metadata, bool) {
md, ok := ctx.Value(metadataCurrentKey{}).(rawMetadata) if ctx == nil {
return nil, false
}
md, ok := ctx.Value(mdKey{}).(*rawMetadata)
if !ok || md.md == nil {
return nil, false
}
return md.md, ok
}
// MustContext returns metadata from the given context
// returned metadata shoud not be modified or race condition happens
func MustContext(ctx context.Context) Metadata {
md, ok := FromContext(ctx)
if !ok { if !ok {
return nil panic("missing metadata")
}
return md
} }
if v, ok := md.md[key]; ok { // NewContext creates a new context with the given metadata
return copyOf(v) func NewContext(ctx context.Context, md Metadata) context.Context {
if ctx == nil {
ctx = context.Background()
} }
for k, v := range md.md { return context.WithValue(ctx, mdKey{}, &rawMetadata{md})
// Case-insensitive comparison: Metadata is a map, and there's no guarantee
// that the Metadata attached to the context is created using our helper
// functions.
if strings.EqualFold(k, key) {
return copyOf(v)
}
}
return nil
} }
// ValueFromIncomingContext retrieves a deep copy of the metadata for the given key // SetOutgoingContext modify outgoing context with given metadata
// from the context, performing a case-insensitive search if needed. Returns nil if not found. func SetOutgoingContext(ctx context.Context, md Metadata) bool {
func ValueFromIncomingContext(ctx context.Context, key string) []string { if ctx == nil {
raw, ok := ctx.Value(metadataIncomingKey{}).(rawMetadata) return false
}
if omd, ok := ctx.Value(mdOutgoingKey{}).(*rawMetadata); ok {
omd.md = md
return true
}
return false
}
// SetIncomingContext modify incoming context with given metadata
func SetIncomingContext(ctx context.Context, md Metadata) bool {
if ctx == nil {
return false
}
if omd, ok := ctx.Value(mdIncomingKey{}).(*rawMetadata); ok {
omd.md = md
return true
}
return false
}
// NewIncomingContext creates a new context with incoming metadata attached
func NewIncomingContext(ctx context.Context, md Metadata) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, mdIncomingKey{}, &rawMetadata{md})
}
// NewOutgoingContext creates a new context with outcoming metadata attached
func NewOutgoingContext(ctx context.Context, md Metadata) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, mdOutgoingKey{}, &rawMetadata{md})
}
// AppendOutgoingContext apends new md to context
func AppendOutgoingContext(ctx context.Context, kv ...string) context.Context {
md, ok := Pairs(kv...)
if !ok { if !ok {
return nil return ctx
} }
omd, ok := FromOutgoingContext(ctx)
if v, ok := raw.md[key]; ok {
return copyOf(v)
}
for k, v := range raw.md {
// Case-insensitive comparison: Metadata is a map, and there's no guarantee
// that the Metadata attached to the context is created using our helper
// functions.
if strings.EqualFold(k, key) {
return copyOf(v)
}
}
return nil
}
// ValueFromOutgoingContext retrieves a deep copy of the metadata for the given key
// from the context, performing a case-insensitive search if needed. Returns nil if not found.
func ValueFromOutgoingContext(ctx context.Context, key string) []string {
md, ok := ctx.Value(metadataOutgoingKey{}).(rawMetadata)
if !ok { if !ok {
return nil return NewOutgoingContext(ctx, md)
}
for k, v := range md {
omd.Set(k, v)
}
return ctx
} }
if v, ok := md.md[key]; ok { // AppendIncomingContext apends new md to context
return copyOf(v) func AppendIncomingContext(ctx context.Context, kv ...string) context.Context {
md, ok := Pairs(kv...)
if !ok {
return ctx
} }
for k, v := range md.md { omd, ok := FromIncomingContext(ctx)
// Case-insensitive comparison: Metadata is a map, and there's no guarantee if !ok {
// that the Metadata attached to the context is created using our helper return NewIncomingContext(ctx, md)
// functions.
if strings.EqualFold(k, key) {
return copyOf(v)
} }
for k, v := range md {
omd.Set(k, v)
} }
return nil return ctx
}
// AppendContext apends new md to context
func AppendContext(ctx context.Context, kv ...string) context.Context {
md, ok := Pairs(kv...)
if !ok {
return ctx
}
omd, ok := FromContext(ctx)
if !ok {
return NewContext(ctx, md)
}
for k, v := range md {
omd.Set(k, v)
}
return ctx
} }

140
metadata/context_test.go Normal file
View File

@@ -0,0 +1,140 @@
package metadata
import (
"context"
"testing"
)
func TestFromNilContext(t *testing.T) {
// nolint: staticcheck
c, ok := FromContext(nil)
if ok || c != nil {
t.Fatal("FromContext not works")
}
}
func TestNewNilContext(t *testing.T) {
// nolint: staticcheck
ctx := NewContext(nil, New(0))
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestFromContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), mdKey{}, &rawMetadata{New(0)})
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("FromContext not works")
}
}
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), New(0))
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestFromIncomingContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), mdIncomingKey{}, &rawMetadata{New(0)})
c, ok := FromIncomingContext(ctx)
if c == nil || !ok {
t.Fatal("FromIncomingContext not works")
}
}
func TestFromOutgoingContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), mdOutgoingKey{}, &rawMetadata{New(0)})
c, ok := FromOutgoingContext(ctx)
if c == nil || !ok {
t.Fatal("FromOutgoingContext not works")
}
}
func TestSetIncomingContext(t *testing.T) {
md := New(1)
md.Set("key", "val")
ctx := context.WithValue(context.TODO(), mdIncomingKey{}, &rawMetadata{})
if !SetIncomingContext(ctx, md) {
t.Fatal("SetIncomingContext not works")
}
md, ok := FromIncomingContext(ctx)
if md == nil || !ok {
t.Fatal("SetIncomingContext not works")
} else if v, ok := md.Get("key"); !ok || v != "val" {
t.Fatal("SetIncomingContext not works")
}
}
func TestSetOutgoingContext(t *testing.T) {
md := New(1)
md.Set("key", "val")
ctx := context.WithValue(context.TODO(), mdOutgoingKey{}, &rawMetadata{})
if !SetOutgoingContext(ctx, md) {
t.Fatal("SetOutgoingContext not works")
}
md, ok := FromOutgoingContext(ctx)
if md == nil || !ok {
t.Fatal("SetOutgoingContext not works")
} else if v, ok := md.Get("key"); !ok || v != "val" {
t.Fatal("SetOutgoingContext not works")
}
}
func TestNewIncomingContext(t *testing.T) {
md := New(1)
md.Set("key", "val")
ctx := NewIncomingContext(context.TODO(), md)
c, ok := FromIncomingContext(ctx)
if c == nil || !ok {
t.Fatal("NewIncomingContext not works")
}
}
func TestNewOutgoingContext(t *testing.T) {
md := New(1)
md.Set("key", "val")
ctx := NewOutgoingContext(context.TODO(), md)
c, ok := FromOutgoingContext(ctx)
if c == nil || !ok {
t.Fatal("NewOutgoingContext not works")
}
}
func TestAppendIncomingContext(t *testing.T) {
md := New(1)
md.Set("key1", "val1")
ctx := AppendIncomingContext(context.TODO(), "key2", "val2")
nmd, ok := FromIncomingContext(ctx)
if nmd == nil || !ok {
t.Fatal("AppendIncomingContext not works")
}
if v, ok := nmd.Get("key2"); !ok || v != "val2" {
t.Fatal("AppendIncomingContext not works")
}
}
func TestAppendOutgoingContext(t *testing.T) {
md := New(1)
md.Set("key1", "val1")
ctx := AppendOutgoingContext(context.TODO(), "key2", "val2")
nmd, ok := FromOutgoingContext(ctx)
if nmd == nil || !ok {
t.Fatal("AppendOutgoingContext not works")
}
if v, ok := nmd.Get("key2"); !ok || v != "val2" {
t.Fatal("AppendOutgoingContext not works")
}
}

View File

@@ -1,19 +0,0 @@
// Package metadata is a way of defining message headers
package metadata
var (
// HeaderTopic is the header name that contains topic name.
HeaderTopic = "Micro-Topic"
// HeaderContentType specifies content type of message.
HeaderContentType = "Content-Type"
// HeaderEndpoint specifies endpoint in service.
HeaderEndpoint = "Micro-Endpoint"
// HeaderService specifies service.
HeaderService = "Micro-Service"
// HeaderTimeout specifies timeout of operation.
HeaderTimeout = "Micro-Timeout"
// HeaderAuthorization specifies Authorization header.
HeaderAuthorization = "Authorization"
// HeaderXRequestID specifies request id.
HeaderXRequestID = "X-Request-Id"
)

View File

@@ -1,7 +0,0 @@
package metadata
func copyOf(v []string) []string {
vals := make([]string, len(v))
copy(vals, v)
return vals
}

View File

@@ -1,37 +0,0 @@
package metadata
import "sort"
type Iterator struct {
md Metadata
keys []string
cur int
cnt int
}
// Next advances the iterator to the next element.
func (iter *Iterator) Next(k *string, v *[]string) bool {
if iter.cur+1 > iter.cnt {
return false
}
if k != nil && v != nil {
*k = iter.keys[iter.cur]
vv := iter.md[*k]
*v = make([]string, len(vv))
copy(*v, vv)
iter.cur++
}
return true
}
// Iterator returns an iterator for iterating over metadata in sorted order.
func (md Metadata) Iterator() *Iterator {
iter := &Iterator{md: md, cnt: len(md)}
iter.keys = make([]string, 0, iter.cnt)
for k := range md {
iter.keys = append(iter.keys, k)
}
sort.Strings(iter.keys)
return iter
}

View File

@@ -1,160 +1,167 @@
// Package metadata is a way of defining message headers
package metadata package metadata
import ( import (
"fmt"
"net/textproto" "net/textproto"
"sort"
"strings" "strings"
) )
// defaultMetadataSize is used when initializing new Metadata. var (
// HeaderTopic is the header name that contains topic name
HeaderTopic = "Micro-Topic"
// HeaderContentType specifies content type of message
HeaderContentType = "Content-Type"
// HeaderEndpoint specifies endpoint in service
HeaderEndpoint = "Micro-Endpoint"
// HeaderService specifies service
HeaderService = "Micro-Service"
// HeaderTimeout specifies timeout of operation
HeaderTimeout = "Micro-Timeout"
// HeaderAuthorization specifies Authorization header
HeaderAuthorization = "Authorization"
// HeaderXRequestID specifies request id
HeaderXRequestID = "X-Request-Id"
)
// Metadata is our way of representing request headers internally.
// They're used at the RPC level and translate back and forth
// from Transport headers.
type Metadata map[string]string
type rawMetadata struct {
md Metadata
}
// defaultMetadataSize used when need to init new Metadata
var defaultMetadataSize = 2 var defaultMetadataSize = 2
// Metadata maps keys to values. Use the New, NewWithMetadata and Pairs functions to create it. // Iterator used to iterate over metadata with order
type Metadata map[string][]string type Iterator struct {
md Metadata
// New creates a zero-value Metadata with the specified size. keys []string
func New(l int) Metadata { cur int
if l == 0 { cnt int
l = defaultMetadataSize
}
md := make(Metadata, l)
return md
} }
// NewWithMetadata creates a Metadata from the provided key-value map. // Next advance iterator to next element
func NewWithMetadata(m map[string]string) Metadata { func (iter *Iterator) Next(k, v *string) bool {
md := make(Metadata, len(m)) if iter.cur+1 > iter.cnt {
for key, val := range m { return false
md[key] = append(md[key], val)
}
return md
} }
// Pairs returns a Metadata formed from the key-value mapping. It panics if the length of kv is odd. *k = iter.keys[iter.cur]
func Pairs(kv ...string) Metadata { *v = iter.md[*k]
iter.cur++
return true
}
// Iterator returns the itarator for metadata in sorted order
func (md Metadata) Iterator() *Iterator {
iter := &Iterator{md: md, cnt: len(md)}
iter.keys = make([]string, 0, iter.cnt)
for k := range md {
iter.keys = append(iter.keys, k)
}
sort.Strings(iter.keys)
return iter
}
func (md Metadata) MustGet(key string) string {
val, ok := md.Get(key)
if !ok {
panic("missing metadata key")
}
return val
}
// Get returns value from metadata by key
func (md Metadata) Get(key string) (string, bool) {
// fast path
val, ok := md[key]
if !ok {
// slow path
val, ok = md[textproto.CanonicalMIMEHeaderKey(key)]
if !ok {
val, ok = md[strings.ToLower(key)]
}
}
return val, ok
}
// Set is used to store value in metadata
func (md Metadata) Set(kv ...string) {
if len(kv)%2 == 1 { if len(kv)%2 == 1 {
panic(fmt.Sprintf("metadata: Pairs got the odd number of input pairs for metadata: %d", len(kv))) kv = kv[:len(kv)-1]
} }
md := make(Metadata, len(kv)/2) for idx := 0; idx < len(kv); idx += 2 {
for i := 0; i < len(kv); i += 2 { md[textproto.CanonicalMIMEHeaderKey(kv[idx])] = kv[idx+1]
md[kv[i]] = append(md[kv[i]], kv[i+1])
} }
return md
} }
// Join combines multiple Metadatas into a single Metadata. // Del is used to remove value from metadata
// The order of values for each key is determined by the order in which the Metadatas are provided to Join. func (md Metadata) Del(keys ...string) {
func Join(mds ...Metadata) Metadata { for _, key := range keys {
out := Metadata{} // fast path
for _, md := range mds { delete(md, key)
// slow path
delete(md, textproto.CanonicalMIMEHeaderKey(key))
// very slow path
delete(md, strings.ToLower(key))
}
}
// Copy makes a copy of the metadata
func (md Metadata) CopyTo(dst Metadata) {
for k, v := range md { for k, v := range md {
out[k] = append(out[k], v...) dst[k] = v
} }
} }
return out
}
// Copy returns a deep copy of Metadata. // Copy makes a copy of the metadata
func Copy(src Metadata) Metadata { func Copy(md Metadata, exclude ...string) Metadata {
out := make(Metadata, len(src)) nmd := New(len(md))
for k, v := range src {
out[k] = copyOf(v)
}
return out
}
// Copy returns a deep copy of Metadata.
func (md Metadata) Copy() Metadata {
out := make(Metadata, len(md))
for k, v := range md { for k, v := range md {
out[k] = copyOf(v) nmd[k] = v
} }
return out nmd.Del(exclude...)
return nmd
} }
// CopyTo performs a deep copy of Metadata to the out. // New return new sized metadata
func (md Metadata) CopyTo(out Metadata) { func New(size int) Metadata {
for k, v := range md { if size == 0 {
out[k] = copyOf(v) size = defaultMetadataSize
} }
return make(Metadata, size)
} }
// Len returns the number of items in Metadata. // Merge merges metadata to existing metadata, overwriting if specified
func (md Metadata) Len() int { func Merge(omd Metadata, mmd Metadata, overwrite bool) Metadata {
return len(md) var ok bool
nmd := Copy(omd)
for key, val := range mmd {
_, ok = nmd[key]
switch {
case ok && !overwrite:
continue
case val != "":
nmd[key] = val
case ok && val == "":
nmd.Del(key)
}
}
return nmd
} }
// AsMap returns a deep copy of Metadata as a map[string]string // Pairs from which metadata created
func (md Metadata) AsMap() map[string]string { func Pairs(kv ...string) (Metadata, bool) {
out := make(map[string]string, len(md)) if len(kv)%2 == 1 {
for k, v := range md { return nil, false
out[k] = strings.Join(v, ",")
} }
return out md := New(len(kv) / 2)
} for idx := 0; idx < len(kv); idx += 2 {
md[kv[idx]] = kv[idx+1]
// AsHTTP1 returns a deep copy of Metadata with keys converted to canonical MIME header key format.
func (md Metadata) AsHTTP1() map[string][]string {
out := make(map[string][]string, len(md))
for k, v := range md {
out[textproto.CanonicalMIMEHeaderKey(k)] = copyOf(v)
}
return out
}
// AsHTTP2 returns a deep copy of Metadata with keys converted to lowercase.
func (md Metadata) AsHTTP2() map[string][]string {
out := make(map[string][]string, len(md))
for k, v := range md {
out[strings.ToLower(k)] = copyOf(v)
}
return out
}
// Get retrieves the values for a given key, checking the key in three formats:
// - exact case,
// - lower case,
// - canonical MIME header key format.
func (md Metadata) Get(k string) []string {
v, ok := md[k]
if !ok {
v, ok = md[strings.ToLower(k)]
}
if !ok {
v = md[textproto.CanonicalMIMEHeaderKey(k)]
}
return v
}
// GetJoined retrieves the values for a given key and joins them into a single string, separated by commas.
func (md Metadata) GetJoined(k string) string {
return strings.Join(md.Get(k), ",")
}
// Set assigns the values to the given key.
func (md Metadata) Set(key string, vals ...string) {
if len(vals) == 0 {
return
}
md[key] = vals
}
// Append adds values to the existing values for the given key.
func (md Metadata) Append(key string, vals ...string) {
if len(vals) == 0 {
return
}
md[key] = append(md[key], vals...)
}
// Del removes the values for the given keys k. It checks and removes the keys in the following formats:
// - exact case,
// - lower case,
// - canonical MIME header key format.
func (md Metadata) Del(k ...string) {
for i := range k {
delete(md, k[i])
delete(md, strings.ToLower(k[i]))
delete(md, textproto.CanonicalMIMEHeaderKey(k[i]))
} }
return md, true
} }

View File

@@ -5,31 +5,11 @@ import (
"testing" "testing"
) )
func TesSet(t *testing.T) {
md := Pairs("key1", "val1", "key2", "val2")
md.Set("key1", "val2", "val3")
v := md.GetJoined("X-Request-Id")
if v != "val2, val3" {
t.Fatal("set not works")
}
}
/*
func TestAppendOutgoingContextModify(t *testing.T) {
md := Pairs("key1", "val1")
ctx := NewOutgoingContext(context.TODO(), md)
nctx := AppendOutgoingContext(ctx, "key1", "val3", "key2", "val2")
_ = nctx
omd := MustOutgoingContext(nctx)
fmt.Printf("%#+v\n", omd)
}
*/
func TestLowercase(t *testing.T) { func TestLowercase(t *testing.T) {
md := New(1) md := New(1)
md["x-request-id"] = []string{"12345"} md["x-request-id"] = "12345"
v := md.GetJoined("X-Request-Id") v, ok := md.Get("X-Request-Id")
if v == "" { if !ok || v == "" {
t.Fatalf("metadata invalid %#+v", md) t.Fatalf("metadata invalid %#+v", md)
} }
} }
@@ -56,13 +36,50 @@ func TestMultipleUsage(t *testing.T) {
_ = omd _ = omd
} }
func TestPairs(t *testing.T) { func TestMetadataSetMultiple(t *testing.T) {
md := Pairs("key1", "val1", "key2", "val2") md := New(4)
if v := md.Get("key1"); v == nil { md.Set("key1", "val1", "key2", "val2", "key3")
if v, ok := md.Get("key1"); !ok || v != "val1" {
t.Fatalf("invalid kv %#+v", md)
}
if v, ok := md.Get("key2"); !ok || v != "val2" {
t.Fatalf("invalid kv %#+v", md)
}
if _, ok := md.Get("key3"); ok {
t.Fatalf("invalid kv %#+v", md)
}
}
func TestAppend(t *testing.T) {
ctx := context.Background()
ctx = AppendIncomingContext(ctx, "key1", "val1", "key2", "val2")
md, ok := FromIncomingContext(ctx)
if !ok {
t.Fatal("metadata empty")
}
if _, ok := md.Get("key1"); !ok {
t.Fatal("key1 not found") t.Fatal("key1 not found")
} }
} }
func TestPairs(t *testing.T) {
md, ok := Pairs("key1", "val1", "key2", "val2")
if !ok {
t.Fatal("odd number of kv")
}
if _, ok = md.Get("key1"); !ok {
t.Fatal("key1 not found")
}
}
func testCtx(ctx context.Context) {
md := New(2)
md.Set("Key1", "Val1_new")
md.Set("Key3", "Val3")
SetOutgoingContext(ctx, md)
}
func TestPassing(t *testing.T) { func TestPassing(t *testing.T) {
ctx := context.TODO() ctx := context.TODO()
md1 := New(2) md1 := New(2)
@@ -70,63 +87,70 @@ func TestPassing(t *testing.T) {
md1.Set("Key2", "Val2") md1.Set("Key2", "Val2")
ctx = NewIncomingContext(ctx, md1) ctx = NewIncomingContext(ctx, md1)
testCtx(ctx)
_, ok := FromOutgoingContext(ctx) _, ok := FromOutgoingContext(ctx)
if ok { if ok {
t.Fatalf("create outgoing context") t.Fatalf("create outgoing context")
} }
ctx = NewOutgoingContext(ctx, md1) ctx = NewOutgoingContext(ctx, New(1))
testCtx(ctx)
md, ok := FromOutgoingContext(ctx) md, ok := FromOutgoingContext(ctx)
if !ok { if !ok {
t.Fatalf("missing metadata from outgoing context") t.Fatalf("missing metadata from outgoing context")
} }
if v := md.Get("Key1"); v == nil || v[0] != "Val1" { if v, ok := md.Get("Key1"); !ok || v != "Val1_new" {
t.Fatalf("invalid metadata value %#+v", md) t.Fatalf("invalid metadata value %#+v", md)
} }
} }
func TestIterator(t *testing.T) { func TestMerge(t *testing.T) {
md := Pairs( omd := Metadata{
"1Last", "last", "key1": "val1",
"2First", "first", }
"3Second", "second", mmd := Metadata{
) "key2": "val2",
}
nmd := Merge(omd, mmd, true)
if len(nmd) != 2 {
t.Fatalf("merge failed: %v", nmd)
}
}
func TestIterator(_ *testing.T) {
md := Metadata{
"1Last": "last",
"2First": "first",
"3Second": "second",
}
iter := md.Iterator() iter := md.Iterator()
var k string var k, v string
var v []string
chk := New(3)
for iter.Next(&k, &v) {
chk[k] = v
}
for k, v := range chk { for iter.Next(&k, &v) {
if cv, ok := md[k]; !ok || len(cv) != len(v) || cv[0] != v[0] { // fmt.Printf("k: %s, v: %s\n", k, v)
t.Fatalf("XXXX %#+v %#+v", chk, md)
}
} }
} }
func TestMedataCanonicalKey(t *testing.T) { func TestMedataCanonicalKey(t *testing.T) {
md := New(1) md := New(1)
md.Set("x-request-id", "12345") md.Set("x-request-id", "12345")
v := md.GetJoined("x-request-id") v, ok := md.Get("x-request-id")
if v == "" { if !ok {
t.Fatalf("failed to get x-request-id") t.Fatalf("failed to get x-request-id")
} else if v != "12345" { } else if v != "12345" {
t.Fatalf("invalid metadata value: %s != %s", "12345", v) t.Fatalf("invalid metadata value: %s != %s", "12345", v)
} }
v = md.GetJoined("X-Request-Id") v, ok = md.Get("X-Request-Id")
if v == "" { if !ok {
t.Fatalf("failed to get x-request-id") t.Fatalf("failed to get x-request-id")
} else if v != "12345" { } else if v != "12345" {
t.Fatalf("invalid metadata value: %s != %s", "12345", v) t.Fatalf("invalid metadata value: %s != %s", "12345", v)
} }
v = md.GetJoined("X-Request-ID") v, ok = md.Get("X-Request-ID")
if v == "" { if !ok {
t.Fatalf("failed to get x-request-id") t.Fatalf("failed to get x-request-id")
} else if v != "12345" { } else if v != "12345" {
t.Fatalf("invalid metadata value: %s != %s", "12345", v) t.Fatalf("invalid metadata value: %s != %s", "12345", v)
@@ -138,8 +162,8 @@ func TestMetadataSet(t *testing.T) {
md.Set("Key", "val") md.Set("Key", "val")
val := md.GetJoined("Key") val, ok := md.Get("Key")
if val == "" { if !ok {
t.Fatal("key Key not found") t.Fatal("key Key not found")
} }
if val != "val" { if val != "val" {
@@ -149,27 +173,36 @@ func TestMetadataSet(t *testing.T) {
func TestMetadataDelete(t *testing.T) { func TestMetadataDelete(t *testing.T) {
md := Metadata{ md := Metadata{
"Foo": []string{"bar"}, "Foo": "bar",
"Baz": []string{"empty"}, "Baz": "empty",
} }
md.Del("Baz") md.Del("Baz")
v := md.Get("Baz") _, ok := md.Get("Baz")
if v != nil { if ok {
t.Fatal("key Baz not deleted") t.Fatal("key Baz not deleted")
} }
} }
func TestNilContext(t *testing.T) {
var ctx context.Context
_, ok := FromContext(ctx)
if ok {
t.Fatal("nil context")
}
}
func TestMetadataCopy(t *testing.T) { func TestMetadataCopy(t *testing.T) {
md := Metadata{ md := Metadata{
"Foo": []string{"bar"}, "Foo": "bar",
"Bar": []string{"baz"}, "Bar": "baz",
} }
cp := Copy(md) cp := Copy(md)
for k, v := range md { for k, v := range md {
if cv := cp[k]; cv[0] != v[0] { if cv := cp[k]; cv != v {
t.Fatalf("Got %s:%s for %s:%s", k, cv, k, v) t.Fatalf("Got %s:%s for %s:%s", k, cv, k, v)
} }
} }
@@ -177,7 +210,7 @@ func TestMetadataCopy(t *testing.T) {
func TestMetadataContext(t *testing.T) { func TestMetadataContext(t *testing.T) {
md := Metadata{ md := Metadata{
"Foo": []string{"bar"}, "Foo": "bar",
} }
ctx := NewContext(context.TODO(), md) ctx := NewContext(context.TODO(), md)
@@ -187,7 +220,7 @@ func TestMetadataContext(t *testing.T) {
t.Errorf("Unexpected error retrieving metadata, got %t", ok) t.Errorf("Unexpected error retrieving metadata, got %t", ok)
} }
if emd["Foo"][0] != md["Foo"][0] { if emd["Foo"] != md["Foo"] {
t.Errorf("Expected key: %s val: %s, got key: %s val: %s", "Foo", md["Foo"], "Foo", emd["Foo"]) t.Errorf("Expected key: %s val: %s, got key: %s val: %s", "Foo", md["Foo"], "Foo", emd["Foo"])
} }
@@ -196,74 +229,13 @@ func TestMetadataContext(t *testing.T) {
} }
} }
func TestFromContext(t *testing.T) { func TestCopy(t *testing.T) {
ctx := context.WithValue(context.TODO(), metadataCurrentKey{}, rawMetadata{md: New(0)}) md := New(2)
md.Set("key1", "val1", "key2", "val2")
c, ok := FromContext(ctx) nmd := Copy(md, "key2")
if c == nil || !ok { if len(nmd) != 1 {
t.Fatal("FromContext not works") t.Fatal("Copy exclude not works")
} } else if nmd["Key1"] != "val1" {
} t.Fatal("Copy exclude not works")
func TestNewContext(t *testing.T) {
ctx := NewContext(context.TODO(), New(0))
c, ok := FromContext(ctx)
if c == nil || !ok {
t.Fatal("NewContext not works")
}
}
func TestFromIncomingContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), metadataIncomingKey{}, rawMetadata{md: New(0)})
c, ok := FromIncomingContext(ctx)
if c == nil || !ok {
t.Fatal("FromIncomingContext not works")
}
}
func TestFromOutgoingContext(t *testing.T) {
ctx := context.WithValue(context.TODO(), metadataOutgoingKey{}, rawMetadata{md: New(0)})
c, ok := FromOutgoingContext(ctx)
if c == nil || !ok {
t.Fatal("FromOutgoingContext not works")
}
}
func TestNewIncomingContext(t *testing.T) {
md := New(1)
md.Set("key", "val")
ctx := NewIncomingContext(context.TODO(), md)
c, ok := FromIncomingContext(ctx)
if c == nil || !ok {
t.Fatal("NewIncomingContext not works")
}
}
func TestNewOutgoingContext(t *testing.T) {
md := New(1)
md.Set("key", "val")
ctx := NewOutgoingContext(context.TODO(), md)
c, ok := FromOutgoingContext(ctx)
if c == nil || !ok {
t.Fatal("NewOutgoingContext not works")
}
}
func TestAppendOutgoingContext(t *testing.T) {
md := New(1)
md.Set("key1", "val1")
ctx := AppendOutgoingContext(context.TODO(), "key2", "val2")
nmd, ok := FromOutgoingContext(ctx)
if nmd == nil || !ok {
t.Fatal("AppendOutgoingContext not works")
}
if v := nmd.GetJoined("key2"); v != "val2" {
t.Fatal("AppendOutgoingContext not works")
} }
} }

View File

@@ -4,8 +4,8 @@ package meter
import ( import (
"io" "io"
"sort" "sort"
"strconv"
"strings" "strings"
"sync"
"time" "time"
) )
@@ -117,39 +117,6 @@ func BuildLabels(labels ...string) []string {
return labels return labels
} }
var spool = newStringsPool(500)
type stringsPool struct {
p *sync.Pool
c int
}
func newStringsPool(size int) *stringsPool {
p := &stringsPool{c: size}
p.p = &sync.Pool{
New: func() interface{} {
return &strings.Builder{}
},
}
return p
}
func (p *stringsPool) Cap() int {
return p.c
}
func (p *stringsPool) Get() *strings.Builder {
return p.p.Get().(*strings.Builder)
}
func (p *stringsPool) Put(b *strings.Builder) {
if b.Cap() > p.c {
return
}
b.Reset()
p.p.Put(b)
}
// BuildName used to combine metric with labels. // BuildName used to combine metric with labels.
// If labels count is odd, drop last element // If labels count is odd, drop last element
func BuildName(name string, labels ...string) string { func BuildName(name string, labels ...string) string {
@@ -158,6 +125,8 @@ func BuildName(name string, labels ...string) string {
} }
if len(labels) > 2 { if len(labels) > 2 {
sort.Sort(byKey(labels))
idx := 0 idx := 0
for { for {
if labels[idx] == labels[idx+2] { if labels[idx] == labels[idx+2] {
@@ -172,9 +141,7 @@ func BuildName(name string, labels ...string) string {
} }
} }
b := spool.Get() var b strings.Builder
defer spool.Put(b)
_, _ = b.WriteString(name) _, _ = b.WriteString(name)
_, _ = b.WriteRune('{') _, _ = b.WriteRune('{')
for idx := 0; idx < len(labels); idx += 2 { for idx := 0; idx < len(labels); idx += 2 {
@@ -182,9 +149,8 @@ func BuildName(name string, labels ...string) string {
_, _ = b.WriteRune(',') _, _ = b.WriteRune(',')
} }
_, _ = b.WriteString(labels[idx]) _, _ = b.WriteString(labels[idx])
_, _ = b.WriteString(`="`) _, _ = b.WriteString(`=`)
_, _ = b.WriteString(labels[idx+1]) _, _ = b.WriteString(strconv.Quote(labels[idx+1]))
_, _ = b.WriteRune('"')
} }
_, _ = b.WriteRune('}') _, _ = b.WriteRune('}')

View File

@@ -50,12 +50,11 @@ func TestBuildName(t *testing.T) {
data := map[string][]string{ data := map[string][]string{
`my_metric{firstlabel="value2",zerolabel="value3"}`: { `my_metric{firstlabel="value2",zerolabel="value3"}`: {
"my_metric", "my_metric",
"firstlabel", "value2", "zerolabel", "value3", "firstlabel", "value2",
"zerolabel", "value3",
}, },
`my_metric{broker="broker2",register="mdns",server="tcp"}`: { `my_metric{broker="broker2",register="mdns",server="tcp"}`: {
"my_metric", "my_metric",
"broker", "broker1", "broker", "broker2", "register", "mdns", "server", "http", "server", "tcp", "broker", "broker1", "broker", "broker2", "server", "http", "server", "tcp", "register", "mdns",
}, },
`my_metric{aaa="aaa"}`: { `my_metric{aaa="aaa"}`: {
"my_metric", "my_metric",

View File

@@ -3,21 +3,21 @@ package micro
import ( import (
"reflect" "reflect"
"go.unistack.org/micro/v4/broker" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/flow" "go.unistack.org/micro/v3/flow"
"go.unistack.org/micro/v4/fsm" "go.unistack.org/micro/v3/fsm"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
"go.unistack.org/micro/v4/router" "go.unistack.org/micro/v3/router"
"go.unistack.org/micro/v4/selector" "go.unistack.org/micro/v3/selector"
"go.unistack.org/micro/v4/server" "go.unistack.org/micro/v3/server"
"go.unistack.org/micro/v4/store" "go.unistack.org/micro/v3/store"
"go.unistack.org/micro/v4/sync" "go.unistack.org/micro/v3/sync"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
func As(b any, target any) bool { func As(b any, target any) bool {

View File

@@ -6,9 +6,8 @@ import (
"reflect" "reflect"
"testing" "testing"
"go.unistack.org/micro/v4/broker" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/fsm" "go.unistack.org/micro/v3/fsm"
"go.unistack.org/micro/v4/metadata"
) )
func TestAs(t *testing.T) { func TestAs(t *testing.T) {
@@ -62,8 +61,6 @@ func TestAs(t *testing.T) {
} }
} }
var _ broker.Broker = (*bro)(nil)
type bro struct { type bro struct {
name string name string
} }
@@ -90,18 +87,23 @@ func (p *bro) Connect(_ context.Context) error { return nil }
// Disconnect disconnect from broker // Disconnect disconnect from broker
func (p *bro) Disconnect(_ context.Context) error { return nil } func (p *bro) Disconnect(_ context.Context) error { return nil }
// NewMessage creates new message
func (p *bro) NewMessage(_ context.Context, _ metadata.Metadata, _ interface{}, _ ...broker.MessageOption) (broker.Message, error) {
return nil, nil
}
// Publish message, msg can be single broker.Message or []broker.Message // Publish message, msg can be single broker.Message or []broker.Message
func (p *bro) Publish(_ context.Context, _ string, _ ...broker.Message) error { func (p *bro) Publish(_ context.Context, _ string, _ *broker.Message, _ ...broker.PublishOption) error {
return nil return nil
} }
// BatchPublish messages to broker with multiple topics
func (p *bro) BatchPublish(_ context.Context, _ []*broker.Message, _ ...broker.PublishOption) error {
return nil
}
// BatchSubscribe subscribes to topic messages via handler
func (p *bro) BatchSubscribe(_ context.Context, _ string, _ broker.BatchHandler, _ ...broker.SubscribeOption) (broker.Subscriber, error) {
return nil, nil
}
// Subscribe subscribes to topic message via handler // Subscribe subscribes to topic message via handler
func (p *bro) Subscribe(_ context.Context, _ string, _ interface{}, _ ...broker.SubscribeOption) (broker.Subscriber, error) { func (p *bro) Subscribe(_ context.Context, _ string, _ broker.Handler, _ ...broker.SubscribeOption) (broker.Subscriber, error) {
return nil, nil return nil, nil
} }

55
network/network.go Normal file
View File

@@ -0,0 +1,55 @@
// Package network is for creating internetworks
package network
import (
"go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v3/server"
)
// Error is network node errors
type Error interface {
// Count is current count of errors
Count() int
// Msg is last error message
Msg() string
}
// Status is node status
type Status interface {
// Error reports error status
Error() Error
}
// Node is network node
type Node interface {
// Id is node id
Id() string
// Address is node bind address
Address() string
// Peers returns node peers
Peers() []Node
// Network is the network node is in
Network() Network
// Status returns node status
Status() Status
}
// Network is micro network
type Network interface {
// Node is network node
Node
// Initialise options
Init(...Option) error
// Options returns the network options
Options() Options
// Name of the network
Name() string
// Connect starts the resolver and tunnel server
Connect() error
// Close stops the tunnel and resolving
Close() error
// Client is micro client
Client() client.Client
// Server is micro server
Server() server.Server
}

135
network/options.go Normal file
View File

@@ -0,0 +1,135 @@
package network
import (
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/network/tunnel"
"go.unistack.org/micro/v3/proxy"
"go.unistack.org/micro/v3/router"
"go.unistack.org/micro/v3/tracer"
"go.unistack.org/micro/v3/util/id"
)
// Option func
type Option func(*Options)
// Options configure network
type Options struct {
// Router used for routing
Router router.Router
// Proxy holds the proxy
Proxy proxy.Proxy
// Logger used for logging
Logger logger.Logger
// Meter used for metrics
Meter meter.Meter
// Tracer used for tracing
Tracer tracer.Tracer
// Tunnel used for transfer data
Tunnel tunnel.Tunnel
// ID of the node
ID string
// Name of the network
Name string
// Address to bind to
Address string
// Advertise sets the address to advertise
Advertise string
// Nodes is a list of nodes to connect to
Nodes []string
}
// ID sets the id of the network node
func ID(id string) Option {
return func(o *Options) {
o.ID = id
}
}
// Name sets the network name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}
// Address sets the network address
func Address(a string) Option {
return func(o *Options) {
o.Address = a
}
}
// Advertise sets the address to advertise
func Advertise(a string) Option {
return func(o *Options) {
o.Advertise = a
}
}
// Nodes is a list of nodes to connect to
func Nodes(n ...string) Option {
return func(o *Options) {
o.Nodes = n
}
}
// Tunnel sets the network tunnel
func Tunnel(t tunnel.Tunnel) Option {
return func(o *Options) {
o.Tunnel = t
}
}
// Router sets the network router
func Router(r router.Router) Option {
return func(o *Options) {
o.Router = r
}
}
// Proxy sets the network proxy
func Proxy(p proxy.Proxy) Option {
return func(o *Options) {
o.Proxy = p
}
}
// Logger sets the network logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// NewOptions returns network default options
func NewOptions(opts ...Option) Options {
options := Options{
ID: id.MustNew(),
Name: "go.micro",
Address: ":0",
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
Tracer: tracer.DefaultTracer,
}
for _, o := range opts {
o(&options)
}
return options
}

View File

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

258
network/transport/memory.go Normal file
View File

@@ -0,0 +1,258 @@
package transport
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
maddr "go.unistack.org/micro/v3/util/addr"
mnet "go.unistack.org/micro/v3/util/net"
"go.unistack.org/micro/v3/util/rand"
)
type memorySocket struct {
ctx context.Context
recv chan *Message
exit chan bool
lexit chan bool
send chan *Message
local string
remote string
timeout time.Duration
sync.RWMutex
}
type memoryClient struct {
*memorySocket
opts DialOptions
}
type memoryListener struct {
lopts ListenOptions
ctx context.Context
exit chan bool
conn chan *memorySocket
addr string
topts Options
sync.RWMutex
}
type memoryTransport struct {
listeners map[string]*memoryListener
opts Options
sync.RWMutex
}
func (ms *memorySocket) Recv(m *Message) error {
ms.RLock()
defer ms.RUnlock()
ctx := ms.ctx
if ms.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ms.ctx, ms.timeout)
defer cancel()
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ms.exit:
return errors.New("connection closed")
case <-ms.lexit:
return errors.New("server connection closed")
case cm := <-ms.recv:
*m = *cm
}
return nil
}
func (ms *memorySocket) Local() string {
return ms.local
}
func (ms *memorySocket) Remote() string {
return ms.remote
}
func (ms *memorySocket) Send(m *Message) error {
ms.RLock()
defer ms.RUnlock()
ctx := ms.ctx
if ms.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ms.ctx, ms.timeout)
defer cancel()
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ms.exit:
return errors.New("connection closed")
case <-ms.lexit:
return errors.New("server connection closed")
case ms.send <- m:
}
return nil
}
func (ms *memorySocket) Close() error {
ms.Lock()
defer ms.Unlock()
select {
case <-ms.exit:
return nil
default:
close(ms.exit)
}
return nil
}
func (m *memoryListener) Addr() string {
return m.addr
}
func (m *memoryListener) Close() error {
m.Lock()
defer m.Unlock()
select {
case <-m.exit:
return nil
default:
close(m.exit)
}
return nil
}
func (m *memoryListener) Accept(fn func(Socket)) error {
for {
select {
case <-m.exit:
return nil
case c := <-m.conn:
go fn(&memorySocket{
lexit: c.lexit,
exit: c.exit,
send: c.recv,
recv: c.send,
local: c.Remote(),
remote: c.Local(),
timeout: m.topts.Timeout,
ctx: m.topts.Context,
})
}
}
}
func (m *memoryTransport) Dial(ctx context.Context, addr string, opts ...DialOption) (Client, error) {
m.RLock()
defer m.RUnlock()
listener, ok := m.listeners[addr]
if !ok {
return nil, errors.New("could not dial " + addr)
}
options := NewDialOptions(opts...)
client := &memoryClient{
&memorySocket{
send: make(chan *Message),
recv: make(chan *Message),
exit: make(chan bool),
lexit: listener.exit,
local: addr,
remote: addr,
timeout: m.opts.Timeout,
ctx: m.opts.Context,
},
options,
}
// pseudo connect
select {
case <-listener.exit:
return nil, errors.New("connection error")
case listener.conn <- client.memorySocket:
}
return client, nil
}
func (m *memoryTransport) Listen(ctx context.Context, addr string, opts ...ListenOption) (Listener, error) {
m.Lock()
defer m.Unlock()
options := NewListenOptions(opts...)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
addr, err = maddr.Extract(host)
if err != nil {
return nil, err
}
// if zero port then randomly assign one
if len(port) > 0 && port == "0" {
var rng rand.Rand
i := rng.Intn(20000)
port = fmt.Sprintf("%d", 10000+i)
}
// set addr with port
addr = mnet.HostPort(addr, port)
if _, ok := m.listeners[addr]; ok {
return nil, errors.New("already listening on " + addr)
}
listener := &memoryListener{
lopts: options,
topts: m.opts,
addr: addr,
conn: make(chan *memorySocket),
exit: make(chan bool),
ctx: m.opts.Context,
}
m.listeners[addr] = listener
return listener, nil
}
func (m *memoryTransport) Init(opts ...Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *memoryTransport) Options() Options {
return m.opts
}
func (m *memoryTransport) String() string {
return "memory"
}
func (m *memoryTransport) Name() string {
return m.opts.Name
}
// NewTransport returns new memory transport with options
func NewTransport(opts ...Option) Transport {
options := NewOptions(opts...)
return &memoryTransport{
opts: options,
listeners: make(map[string]*memoryListener),
}
}

View File

@@ -0,0 +1,100 @@
package transport
import (
"context"
"os"
"testing"
)
func TestMemoryTransport(t *testing.T) {
tr := NewTransport()
ctx := context.Background()
// bind / listen
l, err := tr.Listen(ctx, "127.0.0.1:8080")
if err != nil {
t.Fatalf("Unexpected error listening %v", err)
}
defer l.Close()
cherr := make(chan error, 1)
// accept
go func() {
if nerr := l.Accept(func(sock Socket) {
for {
var m Message
if rerr := sock.Recv(&m); rerr != nil {
cherr <- rerr
return
}
if len(os.Getenv("INTEGRATION_TESTS")) == 0 {
t.Logf("Server Received %s", string(m.Body))
}
if cerr := sock.Send(&Message{
Body: []byte(`pong`),
}); cerr != nil {
cherr <- cerr
return
}
}
}); nerr != nil {
cherr <- err
}
}()
// dial
c, err := tr.Dial(ctx, "127.0.0.1:8080")
if err != nil {
t.Fatalf("Unexpected error dialing %v", err)
}
defer c.Close()
select {
case err := <-cherr:
t.Fatal(err)
default:
// send <=> receive
for i := 0; i < 3; i++ {
if err := c.Send(&Message{
Body: []byte(`ping`),
}); err != nil {
return
}
var m Message
if err := c.Recv(&m); err != nil {
return
}
if len(os.Getenv("INTEGRATION_TESTS")) == 0 {
t.Logf("Client Received %s", string(m.Body))
}
}
}
}
func TestListener(t *testing.T) {
tr := NewTransport()
ctx := context.Background()
// bind / listen on random port
l, err := tr.Listen(ctx, ":0")
if err != nil {
t.Fatalf("Unexpected error listening %v", err)
}
defer l.Close()
// try again
l2, err := tr.Listen(ctx, ":0")
if err != nil {
t.Fatalf("Unexpected error listening %v", err)
}
defer l2.Close()
// now make sure it still fails
l3, err := tr.Listen(ctx, ":8080")
if err != nil {
t.Fatalf("Unexpected error listening %v", err)
}
defer l3.Close()
if _, err := tr.Listen(ctx, ":8080"); err == nil {
t.Fatal("Expected error binding to :8080 got nil")
}
}

View File

@@ -0,0 +1,175 @@
package transport
import (
"context"
"crypto/tls"
"time"
"go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/tracer"
)
// Options struct holds the transport options
type Options struct {
// Meter used for metrics
Meter meter.Meter
// Tracer used for tracing
Tracer tracer.Tracer
// Codec used for marshal/unmarshal messages
Codec codec.Codec
// Logger used for logging
Logger logger.Logger
// Context holds external options
Context context.Context
// TLSConfig holds tls.TLSConfig options
TLSConfig *tls.Config
// Name holds the transport name
Name string
// Addrs holds the transport addrs
Addrs []string
// Timeout holds the timeout
Timeout time.Duration
}
// NewOptions returns new options
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
Tracer: tracer.DefaultTracer,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// DialOptions struct
type DialOptions struct {
// Context holds the external options
Context context.Context
// Timeout holds the timeout
Timeout time.Duration
// Stream flag
Stream bool
}
// NewDialOptions returns new DialOptions
func NewDialOptions(opts ...DialOption) DialOptions {
options := DialOptions{
Timeout: DefaultDialTimeout,
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// ListenOptions struct
type ListenOptions struct {
// TODO: add tls options when listening
// Currently set in global options
// Context holds the external options
Context context.Context
// TLSConfig holds the *tls.Config options
TLSConfig *tls.Config
}
// NewListenOptions returns new ListenOptions
func NewListenOptions(opts ...ListenOption) ListenOptions {
options := ListenOptions{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
return options
}
// Addrs to use for transport
func Addrs(addrs ...string) Option {
return func(o *Options) {
o.Addrs = addrs
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Context sets the context
func Context(ctx context.Context) Option {
return func(o *Options) {
o.Context = ctx
}
}
// Codec sets the codec used for encoding where the transport
// does not support message headers
func Codec(c codec.Codec) Option {
return func(o *Options) {
o.Codec = c
}
}
// Timeout sets the timeout for Send/Recv execution
func Timeout(t time.Duration) Option {
return func(o *Options) {
o.Timeout = t
}
}
// TLSConfig to be used for the transport.
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
// WithStream indicates whether this is a streaming connection
func WithStream() DialOption {
return func(o *DialOptions) {
o.Stream = true
}
}
// WithTimeout used when dialling the remote side
func WithTimeout(d time.Duration) DialOption {
return func(o *DialOptions) {
o.Timeout = d
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}

View File

@@ -0,0 +1,63 @@
// Package transport is an interface for synchronous connection based communication
package transport
import (
"context"
"time"
"go.unistack.org/micro/v3/metadata"
)
var (
// DefaultTransport is the global default transport
DefaultTransport = NewTransport()
// DefaultDialTimeout the default dial timeout
DefaultDialTimeout = time.Second * 5
)
// Transport is an interface which is used for communication between
// services. It uses connection based socket send/recv semantics and
// has various implementations; http, grpc, quic.
type Transport interface {
Init(...Option) error
Options() Options
Dial(ctx context.Context, addr string, opts ...DialOption) (Client, error)
Listen(ctx context.Context, addr string, opts ...ListenOption) (Listener, error)
String() string
}
// Message is used to transfer data
type Message struct {
Header metadata.Metadata
Body []byte
}
// Socket bastraction interface
type Socket interface {
Recv(*Message) error
Send(*Message) error
Close() error
Local() string
Remote() string
}
// Client is the socket owner
type Client interface {
Socket
}
// Listener is the interface for stream oriented messaging
type Listener interface {
Addr() string
Close() error
Accept(func(Socket)) error
}
// Option is the option signature
type Option func(*Options)
// DialOption is the option signature
type DialOption func(*DialOptions)
// ListenOption is the option signature
type ListenOption func(*ListenOptions)

View File

@@ -0,0 +1,372 @@
// Package broker is a tunnel broker
package broker
import (
"context"
"fmt"
"go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v3/network/transport"
"go.unistack.org/micro/v3/network/tunnel"
)
type tunBroker struct {
tunnel tunnel.Tunnel
opts broker.Options
}
type tunSubscriber struct {
listener tunnel.Listener
handler broker.Handler
closed chan bool
topic string
opts broker.SubscribeOptions
}
type tunBatchSubscriber struct {
listener tunnel.Listener
handler broker.BatchHandler
closed chan bool
topic string
opts broker.SubscribeOptions
}
type tunEvent struct {
err error
message *broker.Message
topic string
}
// used to access tunnel from options context
type (
tunnelKey struct{}
tunnelAddr struct{}
)
func (t *tunBroker) Live() bool {
return true
}
func (t *tunBroker) Ready() bool {
return true
}
func (t *tunBroker) Health() bool {
return true
}
func (t *tunBroker) Init(opts ...broker.Option) error {
for _, o := range opts {
o(&t.opts)
}
return nil
}
func (t *tunBroker) Name() string {
return t.opts.Name
}
func (t *tunBroker) Options() broker.Options {
return t.opts
}
func (t *tunBroker) Address() string {
return t.tunnel.Address()
}
func (t *tunBroker) Connect(ctx context.Context) error {
return t.tunnel.Connect(ctx)
}
func (t *tunBroker) Disconnect(ctx context.Context) error {
return t.tunnel.Close(ctx)
}
func (t *tunBroker) BatchPublish(ctx context.Context, msgs []*broker.Message, _ ...broker.PublishOption) error {
// TODO: this is probably inefficient, we might want to just maintain an open connection
// it may be easier to add broadcast to the tunnel
topicMap := make(map[string]tunnel.Session)
var err error
for _, msg := range msgs {
topic, _ := msg.Header.Get(metadata.HeaderTopic)
c, ok := topicMap[topic]
if !ok {
c, err = t.tunnel.Dial(ctx, topic, tunnel.DialMode(tunnel.Multicast))
if err != nil {
return err
}
defer c.Close()
topicMap[topic] = c
}
if err = c.Send(&transport.Message{
Header: msg.Header,
Body: msg.Body,
}); err != nil {
// msg.SetError(err)
return err
}
}
return nil
}
func (t *tunBroker) Publish(ctx context.Context, topic string, m *broker.Message, _ ...broker.PublishOption) error {
// TODO: this is probably inefficient, we might want to just maintain an open connection
// it may be easier to add broadcast to the tunnel
c, err := t.tunnel.Dial(ctx, topic, tunnel.DialMode(tunnel.Multicast))
if err != nil {
return err
}
defer c.Close()
return c.Send(&transport.Message{
Header: m.Header,
Body: m.Body,
})
}
func (t *tunBroker) BatchSubscribe(ctx context.Context, topic string, h broker.BatchHandler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
l, err := t.tunnel.Listen(ctx, topic, tunnel.ListenMode(tunnel.Multicast))
if err != nil {
return nil, err
}
tunSub := &tunBatchSubscriber{
topic: topic,
handler: h,
opts: broker.NewSubscribeOptions(opts...),
closed: make(chan bool),
listener: l,
}
// start processing
go tunSub.run()
return tunSub, nil
}
func (t *tunBroker) Subscribe(ctx context.Context, topic string, h broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
l, err := t.tunnel.Listen(ctx, topic, tunnel.ListenMode(tunnel.Multicast))
if err != nil {
return nil, err
}
tunSub := &tunSubscriber{
topic: topic,
handler: h,
opts: broker.NewSubscribeOptions(opts...),
closed: make(chan bool),
listener: l,
}
// start processing
go tunSub.run()
return tunSub, nil
}
func (t *tunBroker) String() string {
return "tunnel"
}
func (t *tunBatchSubscriber) run() {
for {
// accept a new connection
c, err := t.listener.Accept()
if err != nil {
select {
case <-t.closed:
return
default:
continue
}
}
// receive message
m := new(transport.Message)
if err := c.Recv(m); err != nil {
if logger.DefaultLogger.V(logger.ErrorLevel) {
logger.DefaultLogger.Error(t.opts.Context, err.Error(), err)
}
if err = c.Close(); err != nil {
if logger.DefaultLogger.V(logger.ErrorLevel) {
logger.DefaultLogger.Error(t.opts.Context, err.Error(), err)
}
}
continue
}
// close the connection
c.Close()
evts := broker.Events{&tunEvent{
topic: t.topic,
message: &broker.Message{
Header: m.Header,
Body: m.Body,
},
}}
// handle the message
go func() {
_ = t.handler(evts)
}()
}
}
func (t *tunSubscriber) run() {
for {
// accept a new connection
c, err := t.listener.Accept()
if err != nil {
select {
case <-t.closed:
return
default:
continue
}
}
// receive message
m := new(transport.Message)
if err := c.Recv(m); err != nil {
if logger.DefaultLogger.V(logger.ErrorLevel) {
logger.DefaultLogger.Error(t.opts.Context, err.Error(), err)
}
if err = c.Close(); err != nil {
if logger.DefaultLogger.V(logger.ErrorLevel) {
logger.DefaultLogger.Error(t.opts.Context, err.Error(), err)
}
}
continue
}
// close the connection
c.Close()
// handle the message
go func() {
_ = t.handler(&tunEvent{
topic: t.topic,
message: &broker.Message{
Header: m.Header,
Body: m.Body,
},
})
}()
}
}
func (t *tunBatchSubscriber) Options() broker.SubscribeOptions {
return t.opts
}
func (t *tunBatchSubscriber) Topic() string {
return t.topic
}
func (t *tunBatchSubscriber) Unsubscribe(ctx context.Context) error {
select {
case <-t.closed:
return nil
default:
close(t.closed)
return t.listener.Close()
}
}
func (t *tunSubscriber) Options() broker.SubscribeOptions {
return t.opts
}
func (t *tunSubscriber) Topic() string {
return t.topic
}
func (t *tunSubscriber) Unsubscribe(ctx context.Context) error {
select {
case <-t.closed:
return nil
default:
close(t.closed)
return t.listener.Close()
}
}
func (t *tunEvent) Topic() string {
return t.topic
}
func (t *tunEvent) Message() *broker.Message {
return t.message
}
func (t *tunEvent) Ack() error {
return nil
}
func (t *tunEvent) Error() error {
return t.err
}
func (t *tunEvent) SetError(err error) {
t.err = err
}
func (t *tunEvent) Context() context.Context {
return context.TODO()
}
// NewBroker returns new tunnel broker
func NewBroker(opts ...broker.Option) (broker.Broker, error) {
options := broker.NewOptions(opts...)
t, ok := options.Context.Value(tunnelKey{}).(tunnel.Tunnel)
if !ok {
return nil, fmt.Errorf("tunnel not set")
}
a, ok := options.Context.Value(tunnelAddr{}).(string)
if ok {
// initialise address
if err := t.Init(tunnel.Address(a)); err != nil {
return nil, err
}
}
if len(options.Addrs) > 0 {
// initialise nodes
if err := t.Init(tunnel.Nodes(options.Addrs...)); err != nil {
return nil, err
}
}
return &tunBroker{
opts: options,
tunnel: t,
}, nil
}
// WithAddress sets the tunnel address
func WithAddress(a string) broker.Option {
return func(o *broker.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, tunnelAddr{}, a)
}
}
// WithTunnel sets the internal tunnel
func WithTunnel(t tunnel.Tunnel) broker.Option {
return func(o *broker.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, tunnelKey{}, t)
}
}

192
network/tunnel/options.go Normal file
View File

@@ -0,0 +1,192 @@
package tunnel
import (
"time"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/network/transport"
"go.unistack.org/micro/v3/tracer"
"go.unistack.org/micro/v3/util/id"
)
var (
// DefaultAddress is default tunnel bind address
DefaultAddress = ":0"
// DefaultToken the shared default token
DefaultToken = "go.micro.tunnel"
)
// Option func signature
type Option func(*Options)
// Options provides network configuration options
type Options struct {
// Logger used for logging
Logger logger.Logger
// Meter used for metrics
Meter meter.Meter
// Tracer used for tracing
Tracer tracer.Tracer
// Transport used for communication
Transport transport.Transport
// Token the shared auth token
Token string
// Name holds the tunnel name
Name string
// ID holds the tunnel id
ID string
// Address holds the tunnel address
Address string
// Nodes holds the tunnel nodes
Nodes []string
}
// DialOption func
type DialOption func(*DialOptions)
// DialOptions provides dial options
type DialOptions struct {
// Link specifies the link to use
Link string
// specify mode of the session
Mode Mode
// Wait for connection to be accepted
Wait bool
// the dial timeout
Timeout time.Duration
}
// ListenOption func
type ListenOption func(*ListenOptions)
// ListenOptions provides listen options
type ListenOptions struct {
// Mode specify mode of the session
Mode Mode
// Timeout the read timeout
Timeout time.Duration
}
// ID sets the tunnel id
func ID(id string) Option {
return func(o *Options) {
o.ID = id
}
}
// Logger sets the logger
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// Meter sets the meter
func Meter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// Address sets the tunnel address
func Address(a string) Option {
return func(o *Options) {
o.Address = a
}
}
// Nodes specify remote network nodes
func Nodes(n ...string) Option {
return func(o *Options) {
o.Nodes = n
}
}
// Token sets the shared token for auth
func Token(t string) Option {
return func(o *Options) {
o.Token = t
}
}
// Transport listens for incoming connections
func Transport(t transport.Transport) Option {
return func(o *Options) {
o.Transport = t
}
}
// ListenMode option
func ListenMode(m Mode) ListenOption {
return func(o *ListenOptions) {
o.Mode = m
}
}
// ListenTimeout for reads and writes on the listener session
func ListenTimeout(t time.Duration) ListenOption {
return func(o *ListenOptions) {
o.Timeout = t
}
}
// DialMode multicast sets the multicast option to send only to those mapped
func DialMode(m Mode) DialOption {
return func(o *DialOptions) {
o.Mode = m
}
}
// DialTimeout sets the dial timeout of the connection
func DialTimeout(t time.Duration) DialOption {
return func(o *DialOptions) {
o.Timeout = t
}
}
// DialLink specifies the link to pin this connection to.
// This is not applicable if the multicast option is set.
func DialLink(id string) DialOption {
return func(o *DialOptions) {
o.Link = id
}
}
// DialWait specifies whether to wait for the connection
// to be accepted before returning the session
func DialWait(b bool) DialOption {
return func(o *DialOptions) {
o.Wait = b
}
}
// NewOptions returns router default options with filled values
func NewOptions(opts ...Option) Options {
options := Options{
ID: id.MustNew(),
Address: DefaultAddress,
Token: DefaultToken,
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
Tracer: tracer.DefaultTracer,
}
for _, o := range opts {
o(&options)
}
return options
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}
// Name sets the name
func Name(n string) Option {
return func(o *Options) {
o.Name = n
}
}

View File

@@ -0,0 +1,30 @@
package transport
import (
"go.unistack.org/micro/v3/network/transport"
"go.unistack.org/micro/v3/network/tunnel"
)
type tunListener struct {
l tunnel.Listener
}
func (t *tunListener) Addr() string {
return t.l.Channel()
}
func (t *tunListener) Close() error {
return t.l.Close()
}
func (t *tunListener) Accept(fn func(socket transport.Socket)) error {
for {
// accept connection
c, err := t.l.Accept()
if err != nil {
return err
}
// execute the function
go fn(c)
}
}

View File

@@ -0,0 +1,113 @@
// Package transport provides a tunnel transport
package transport
import (
"context"
"fmt"
"go.unistack.org/micro/v3/network/transport"
"go.unistack.org/micro/v3/network/tunnel"
)
type tunTransport struct {
tunnel tunnel.Tunnel
options transport.Options
}
type tunnelKey struct{}
type transportKey struct{}
func (t *tunTransport) Init(opts ...transport.Option) error {
for _, o := range opts {
o(&t.options)
}
// close the existing tunnel
if t.tunnel != nil {
t.tunnel.Close(context.TODO())
}
// get the tunnel
tun, ok := t.options.Context.Value(tunnelKey{}).(tunnel.Tunnel)
if !ok {
return fmt.Errorf("tunnel not set")
}
// get the transport
tr, ok := t.options.Context.Value(transportKey{}).(transport.Transport)
if ok {
_ = tun.Init(tunnel.Transport(tr))
}
// set the tunnel
t.tunnel = tun
return nil
}
func (t *tunTransport) Dial(ctx context.Context, addr string, opts ...transport.DialOption) (transport.Client, error) {
if err := t.tunnel.Connect(ctx); err != nil {
return nil, err
}
c, err := t.tunnel.Dial(ctx, addr)
if err != nil {
return nil, err
}
return c, nil
}
func (t *tunTransport) Listen(ctx context.Context, addr string, opts ...transport.ListenOption) (transport.Listener, error) {
if err := t.tunnel.Connect(ctx); err != nil {
return nil, err
}
l, err := t.tunnel.Listen(ctx, addr)
if err != nil {
return nil, err
}
return &tunListener{l}, nil
}
func (t *tunTransport) Options() transport.Options {
return t.options
}
func (t *tunTransport) String() string {
return "tunnel"
}
// NewTransport honours the initialiser used in
func NewTransport(opts ...transport.Option) transport.Transport {
t := &tunTransport{
options: transport.Options{},
}
// initialise
// t.Init(opts...)
return t
}
// WithTunnel sets the internal tunnel
func WithTunnel(t tunnel.Tunnel) transport.Option {
return func(o *transport.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, tunnelKey{}, t)
}
}
// WithTransport sets the internal transport
func WithTransport(t transport.Transport) transport.Option {
return func(o *transport.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, transportKey{}, t)
}
}

106
network/tunnel/tunnel.go Normal file
View File

@@ -0,0 +1,106 @@
// Package tunnel provides gre network tunnelling
package tunnel
import (
"context"
"errors"
"time"
"go.unistack.org/micro/v3/network/transport"
)
// DefaultTunnel contains default tunnel implementation
var DefaultTunnel Tunnel
const (
// Unicast send over one link
Unicast Mode = iota
// Multicast send to all channel listeners
Multicast
// Broadcast send to all links
Broadcast
)
var (
// DefaultDialTimeout is the dial timeout if none is specified
DefaultDialTimeout = time.Second * 5
// ErrDialTimeout is returned by a call to Dial where the timeout occurs
ErrDialTimeout = errors.New("dial timeout")
// ErrDiscoverChan is returned when we failed to receive the "announce" back from a discovery
ErrDiscoverChan = errors.New("failed to discover channel")
// ErrLinkNotFound is returned when a link is specified at dial time and does not exist
ErrLinkNotFound = errors.New("link not found")
// ErrLinkDisconnected is returned when a link we attempt to send to is disconnected
ErrLinkDisconnected = errors.New("link not connected")
// ErrLinkLoopback is returned when attempting to send an outbound message over loopback link
ErrLinkLoopback = errors.New("link is loopback")
// ErrLinkRemote is returned when attempting to send a loopback message over remote link
ErrLinkRemote = errors.New("link is remote")
// ErrReadTimeout is a timeout on session.Recv
ErrReadTimeout = errors.New("read timeout")
// ErrDecryptingData is for when theres a nonce error
ErrDecryptingData = errors.New("error decrypting data")
)
// Mode of the session
type Mode uint8
// Tunnel creates a gre tunnel on top of the micro/transport.
// It establishes multiple streams using the Micro-Tunnel-Channel header
// and Micro-Tunnel-Session header. The tunnel id is a hash of
// the address being requested.
type Tunnel interface {
// Init initializes tunnel with options
Init(opts ...Option) error
// Address returns the address the tunnel is listening on
Address() string
// Connect connects the tunnel
Connect(ctx context.Context) error
// Close closes the tunnel
Close(ctx context.Context) error
// Links returns all the links the tunnel is connected to
Links() []Link
// Dial allows a client to connect to a channel
Dial(ctx context.Context, channel string, opts ...DialOption) (Session, error)
// Listen allows to accept connections on a channel
Listen(ctx context.Context, channel string, opts ...ListenOption) (Listener, error)
// String returns the name of the tunnel implementation
String() string
}
// Link represents internal links to the tunnel
type Link interface {
// Id returns the link unique Id
Id() string
// Delay is the current load on the link (lower is better)
Delay() int64
// Length returns the roundtrip time as nanoseconds (lower is better)
Length() int64
// Current transfer rate as bits per second (lower is better)
Rate() float64
// Is this a loopback link
Loopback() bool
// State of the link: connected/closed/error
State() string
// honours transport socket
transport.Socket
}
// Listener provides similar constructs to the transport.Listener
type Listener interface {
Accept() (Session, error)
Channel() string
Close() error
}
// Session is a unique session created when dialling or accepting connections on the tunnel
type Session interface {
// The unique session id
Id() string
// The channel name
Channel() string
// The link the session is on
Link() string
// a transport socket
transport.Socket
}

View File

@@ -5,17 +5,17 @@ import (
"fmt" "fmt"
"time" "time"
"go.unistack.org/micro/v4/broker" "go.unistack.org/micro/v3/broker"
"go.unistack.org/micro/v4/client" "go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v4/config" "go.unistack.org/micro/v3/config"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v4/router" "go.unistack.org/micro/v3/router"
"go.unistack.org/micro/v4/server" "go.unistack.org/micro/v3/server"
"go.unistack.org/micro/v4/store" "go.unistack.org/micro/v3/store"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Options for micro service // Options for micro service

View File

@@ -1,222 +0,0 @@
package options
import (
"reflect"
"strings"
"time"
"github.com/spf13/cast"
mreflect "go.unistack.org/micro/v4/util/reflect"
)
// Options interface must be used by all options
type Validator interface {
// Validate returns nil, if all options are correct,
// otherwise returns an error explaining the mistake
Validate() error
}
// Option func signature
type Option func(interface{}) error
// Apply assign options to struct src
func Apply(src interface{}, opts ...Option) error {
for _, opt := range opts {
if err := opt(src); err != nil {
return err
}
}
return nil
}
// SetValueByPath set src struct field to val dst via path
func SetValueByPath(src interface{}, dst interface{}, path string) error {
var err error
switch v := dst.(type) {
case []interface{}:
if len(v) == 1 {
dst = v[0]
}
}
var sv reflect.Value
switch t := src.(type) {
case reflect.Value:
sv = t
default:
sv = reflect.ValueOf(src)
}
parts := strings.Split(path, ".")
for _, p := range parts {
if sv.Kind() == reflect.Ptr {
sv = sv.Elem()
}
if sv.Kind() != reflect.Struct {
return mreflect.ErrInvalidStruct
}
typ := sv.Type()
for idx := 0; idx < typ.NumField(); idx++ {
fld := typ.Field(idx)
val := sv.Field(idx)
/*
if len(fld.PkgPath) != 0 {
continue
}
*/
if fld.Anonymous {
if len(parts) == 1 && val.Kind() == reflect.Struct {
if err = SetValueByPath(val, dst, p); err != nil {
return err
}
}
}
if fld.Name != p && !strings.EqualFold(strings.ToLower(fld.Name), strings.ToLower(p)) {
continue
}
switch val.Interface().(type) {
case []time.Duration:
dst, err = cast.ToDurationSliceE(dst)
if err != nil {
return err
}
reflect.Copy(val, reflect.ValueOf(dst))
return nil
case time.Duration:
dst, err = cast.ToDurationE(dst)
if err != nil {
return err
}
val.Set(reflect.ValueOf(dst))
return nil
case time.Time:
dst, err = cast.ToTimeE(dst)
if err != nil {
return err
}
val.Set(reflect.ValueOf(dst))
return nil
}
switch val.Kind() {
case reflect.Map:
if val.IsZero() {
val.Set(reflect.MakeMap(val.Type()))
}
return setMap(val.Interface(), dst)
case reflect.Array, reflect.Slice:
switch val.Type().Elem().Kind() {
case reflect.Bool:
dst, err = cast.ToBoolSliceE(dst)
case reflect.String:
dst, err = cast.ToStringSliceE(dst)
case reflect.Float32:
dst, err = toFloat32SliceE(dst)
case reflect.Float64:
dst, err = toFloat64SliceE(dst)
case reflect.Int8:
dst, err = toInt8SliceE(dst)
case reflect.Int:
dst, err = cast.ToIntSliceE(dst)
case reflect.Int16:
dst, err = toInt16SliceE(dst)
case reflect.Int32:
dst, err = toInt32SliceE(dst)
case reflect.Int64:
dst, err = toInt64SliceE(dst)
case reflect.Uint8:
dst, err = toUint8SliceE(dst)
case reflect.Uint:
dst, err = toUintSliceE(dst)
case reflect.Uint16:
dst, err = toUint16SliceE(dst)
case reflect.Uint32:
dst, err = toUint32SliceE(dst)
case reflect.Uint64:
dst, err = toUint64SliceE(dst)
}
if err != nil {
return err
}
if val.Kind() == reflect.Slice {
val.Set(reflect.ValueOf(dst))
} else {
reflect.Copy(val, reflect.ValueOf(dst))
}
return nil
case reflect.Float32:
dst, err = toFloat32SliceE(dst)
case reflect.Float64:
dst, err = toFloat64SliceE(dst)
case reflect.Bool:
dst, err = cast.ToBoolE(dst)
case reflect.String:
dst, err = cast.ToStringE(dst)
case reflect.Int8:
dst, err = cast.ToInt8E(dst)
case reflect.Int:
dst, err = cast.ToIntE(dst)
case reflect.Int16:
dst, err = cast.ToInt16E(dst)
case reflect.Int32:
dst, err = cast.ToInt32E(dst)
case reflect.Int64:
dst, err = cast.ToInt64E(dst)
case reflect.Uint8:
dst, err = cast.ToUint8E(dst)
case reflect.Uint:
dst, err = cast.ToUintE(dst)
case reflect.Uint16:
dst, err = cast.ToUint16E(dst)
case reflect.Uint32:
dst, err = cast.ToUint32E(dst)
case reflect.Uint64:
dst, err = cast.ToUint64E(dst)
default:
}
if err != nil {
return err
}
val.Set(reflect.ValueOf(dst))
}
}
return nil
}
// NewOption create new option with name
func NewOption(name string) func(...interface{}) Option {
return func(dst ...interface{}) Option {
return func(src interface{}) error {
return SetValueByPath(src, dst, name)
}
}
}
var (
Address = NewOption("Address")
Name = NewOption("Name")
Broker = NewOption("Broker")
Logger = NewOption("Logger")
Meter = NewOption("Meter")
Tracer = NewOption("Tracer")
Store = NewOption("Store")
Register = NewOption("Register")
Router = NewOption("Router")
Codec = NewOption("Codec")
Codecs = NewOption("Codecs")
Client = NewOption("Client")
Context = NewOption("Context")
TLSConfig = NewOption("TLSConfig")
Metadata = NewOption("Metadata")
Timeout = NewOption("Timeout")
)

View File

@@ -1,181 +0,0 @@
package options_test
import (
"crypto/tls"
"sync"
"testing"
"go.unistack.org/micro/v4/options"
)
type codec interface {
Marshal(v interface{}, opts ...options.Option) ([]byte, error)
Unmarshal(b []byte, v interface{}, opts ...options.Option) error
String() string
}
func TestCodecs(t *testing.T) {
type s struct {
Codecs map[string]codec
}
wg := &sync.WaitGroup{}
tc := &tls.Config{InsecureSkipVerify: true}
opts := []options.Option{
options.NewOption("Codecs")(wg),
options.NewOption("TLSConfig")(tc),
}
src := &s{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
}
func TestSpecial(t *testing.T) {
type s struct {
Wait *sync.WaitGroup
TLSConfig *tls.Config
}
wg := &sync.WaitGroup{}
tc := &tls.Config{InsecureSkipVerify: true}
opts := []options.Option{
options.NewOption("Wait")(wg),
options.NewOption("TLSConfig")(tc),
}
src := &s{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
if src.Wait == nil {
t.Fatalf("failed to set Wait %#+v", src)
}
if src.TLSConfig == nil {
t.Fatalf("failed to set TLSConfig %#+v", src)
}
if src.TLSConfig.InsecureSkipVerify != true {
t.Fatalf("failed to set TLSConfig %#+v", src)
}
}
func TestNested(t *testing.T) {
type server struct {
Address []string
}
type ownserver struct {
server
OwnField string
}
opts := []options.Option{
options.Address("host:port"),
options.NewOption("OwnField")("fieldval"),
}
src := &ownserver{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
if src.Address[0] != "host:port" {
t.Fatalf("failed to set Address %#+v", src)
}
if src.OwnField != "fieldval" {
t.Fatalf("failed to set OwnField %#+v", src)
}
}
func TestAddress(t *testing.T) {
type s struct {
Address []string
}
opts := []options.Option{options.Address("host:port")}
src := &s{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
if src.Address[0] != "host:port" {
t.Fatalf("failed to set Address %#+v", src)
}
}
func TestNewOption(t *testing.T) {
type s struct {
Address []string
}
opts := []options.Option{options.NewOption("Address")("host1:port1", "host2:port2")}
src := &s{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
if src.Address[0] != "host1:port1" {
t.Fatalf("failed to set Address %#+v", src)
}
if src.Address[1] != "host2:port2" {
t.Fatalf("failed to set Address %#+v", src)
}
}
func TestArray(t *testing.T) {
type s struct {
Address [1]string
}
opts := []options.Option{options.NewOption("Address")("host:port", "host1:port1")}
src := &s{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
if src.Address[0] != "host:port" {
t.Fatalf("failed to set Address %#+v", src)
}
}
func TestMap(t *testing.T) {
type s struct {
Metadata map[string]string
}
opts := []options.Option{
options.NewOption("Metadata")("key1", "val1"),
options.NewOption("Metadata")(map[string]string{"key2": "val2"}),
}
src := &s{}
if err := options.Apply(src, opts...); err != nil {
t.Fatal(err)
}
if len(src.Metadata) != 2 {
t.Fatalf("failed to set Metadata %#+v", src)
}
if src.Metadata["key1"] != "val1" {
t.Fatalf("failed to set Metadata %#+v", src)
}
if src.Metadata["key2"] != "val2" {
t.Fatalf("failed to set Metadata %#+v", src)
}
}

View File

@@ -1,577 +0,0 @@
package options
import (
"fmt"
"reflect"
"github.com/spf13/cast"
)
func toInt8SliceE(i interface{}) ([]int8, error) {
if i == nil {
return []int8{}, fmt.Errorf("unable to cast %#v of type %T to []int8", i, i)
}
switch v := i.(type) {
case []int8:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]int8, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToInt8E(s.Index(j).Interface())
if err != nil {
return []int8{}, fmt.Errorf("unable to cast %#v of type %T to []int8", i, i)
}
a[j] = val
}
return a, nil
default:
return []int8{}, fmt.Errorf("unable to cast %#v of type %T to []int8", i, i)
}
}
func toInt16SliceE(i interface{}) ([]int16, error) {
if i == nil {
return []int16{}, fmt.Errorf("unable to cast %#v of type %T to []int16", i, i)
}
switch v := i.(type) {
case []int16:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]int16, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToInt16E(s.Index(j).Interface())
if err != nil {
return []int16{}, fmt.Errorf("unable to cast %#v of type %T to []int16", i, i)
}
a[j] = val
}
return a, nil
default:
return []int16{}, fmt.Errorf("unable to cast %#v of type %T to []int16", i, i)
}
}
func toInt32SliceE(i interface{}) ([]int32, error) {
if i == nil {
return []int32{}, fmt.Errorf("unable to cast %#v of type %T to []int32", i, i)
}
switch v := i.(type) {
case []int32:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]int32, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToInt32E(s.Index(j).Interface())
if err != nil {
return []int32{}, fmt.Errorf("unable to cast %#v of type %T to []int32", i, i)
}
a[j] = val
}
return a, nil
default:
return []int32{}, fmt.Errorf("unable to cast %#v of type %T to []int32", i, i)
}
}
func toInt64SliceE(i interface{}) ([]int64, error) {
if i == nil {
return []int64{}, fmt.Errorf("unable to cast %#v of type %T to []int64", i, i)
}
switch v := i.(type) {
case []int64:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]int64, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToInt64E(s.Index(j).Interface())
if err != nil {
return []int64{}, fmt.Errorf("unable to cast %#v of type %T to []int64", i, i)
}
a[j] = val
}
return a, nil
default:
return []int64{}, fmt.Errorf("unable to cast %#v of type %T to []int64", i, i)
}
}
func toUintSliceE(i interface{}) ([]uint, error) {
if i == nil {
return []uint{}, fmt.Errorf("unable to cast %#v of type %T to []uint", i, i)
}
switch v := i.(type) {
case []uint:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]uint, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToUintE(s.Index(j).Interface())
if err != nil {
return []uint{}, fmt.Errorf("unable to cast %#v of type %T to []uint", i, i)
}
a[j] = val
}
return a, nil
default:
return []uint{}, fmt.Errorf("unable to cast %#v of type %T to []uint", i, i)
}
}
func toUint8SliceE(i interface{}) ([]uint8, error) {
if i == nil {
return []uint8{}, fmt.Errorf("unable to cast %#v of type %T to []uint8", i, i)
}
switch v := i.(type) {
case []uint8:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]uint8, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToUint8E(s.Index(j).Interface())
if err != nil {
return []uint8{}, fmt.Errorf("unable to cast %#v of type %T to []uint8", i, i)
}
a[j] = val
}
return a, nil
default:
return []uint8{}, fmt.Errorf("unable to cast %#v of type %T to []uint8", i, i)
}
}
func toUint16SliceE(i interface{}) ([]uint16, error) {
if i == nil {
return []uint16{}, fmt.Errorf("unable to cast %#v of type %T to []uint16", i, i)
}
switch v := i.(type) {
case []uint16:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]uint16, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToUint16E(s.Index(j).Interface())
if err != nil {
return []uint16{}, fmt.Errorf("unable to cast %#v of type %T to []uint16", i, i)
}
a[j] = val
}
return a, nil
default:
return []uint16{}, fmt.Errorf("unable to cast %#v of type %T to []uint16", i, i)
}
}
func toUint32SliceE(i interface{}) ([]uint32, error) {
if i == nil {
return []uint32{}, fmt.Errorf("unable to cast %#v of type %T to []uint32", i, i)
}
switch v := i.(type) {
case []uint32:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]uint32, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToUint32E(s.Index(j).Interface())
if err != nil {
return []uint32{}, fmt.Errorf("unable to cast %#v of type %T to []uint32", i, i)
}
a[j] = val
}
return a, nil
default:
return []uint32{}, fmt.Errorf("unable to cast %#v of type %T to []uint32", i, i)
}
}
func toUint64SliceE(i interface{}) ([]uint64, error) {
if i == nil {
return []uint64{}, fmt.Errorf("unable to cast %#v of type %T to []uint64", i, i)
}
switch v := i.(type) {
case []uint64:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]uint64, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToUint64E(s.Index(j).Interface())
if err != nil {
return []uint64{}, fmt.Errorf("unable to cast %#v of type %T to []uint64", i, i)
}
a[j] = val
}
return a, nil
default:
return []uint64{}, fmt.Errorf("unable to cast %#v of type %T to []uint64", i, i)
}
}
func toFloat32SliceE(i interface{}) ([]float32, error) {
if i == nil {
return []float32{}, fmt.Errorf("unable to cast %#v of type %T to []float32", i, i)
}
switch v := i.(type) {
case []float32:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]float32, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToFloat32E(s.Index(j).Interface())
if err != nil {
return []float32{}, fmt.Errorf("unable to cast %#v of type %T to []float32", i, i)
}
a[j] = val
}
return a, nil
default:
return []float32{}, fmt.Errorf("unable to cast %#v of type %T to []float32", i, i)
}
}
func toFloat64SliceE(i interface{}) ([]float64, error) {
if i == nil {
return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i)
}
switch v := i.(type) {
case []float64:
return v, nil
}
kind := reflect.TypeOf(i).Kind()
switch kind {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(i)
a := make([]float64, s.Len())
for j := 0; j < s.Len(); j++ {
val, err := cast.ToFloat64E(s.Index(j).Interface())
if err != nil {
return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i)
}
a[j] = val
}
return a, nil
default:
return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float32", i, i)
}
}
func setMap(src interface{}, dst interface{}) error {
var err error
if src == nil {
return fmt.Errorf("unable to cast %#v of type %T", src, src)
}
if dst == nil {
return fmt.Errorf("unable to cast %#v of type %T", dst, dst)
}
val := reflect.ValueOf(src)
keyKind := val.Type().Key().Kind()
valKind := val.Type().Elem().Kind()
switch v := dst.(type) {
case []interface{}:
if len(v) == 1 {
dstVal := reflect.ValueOf(v[0])
if dstVal.Kind() != reflect.Map {
return nil
}
mapIter := dstVal.MapRange()
for mapIter.Next() {
var (
keyVal interface{}
valVal interface{}
)
switch keyKind {
case reflect.Bool:
keyVal, err = cast.ToBoolE(mapIter.Key())
case reflect.String:
keyVal, err = cast.ToStringE(mapIter.Key())
case reflect.Float32:
keyVal, err = cast.ToFloat32E(mapIter.Key())
case reflect.Float64:
keyVal, err = cast.ToFloat64E(mapIter.Key())
case reflect.Int8:
keyVal, err = cast.ToInt8E(mapIter.Key())
case reflect.Int:
keyVal, err = cast.ToIntE(mapIter.Key())
case reflect.Int16:
keyVal, err = cast.ToInt16E(mapIter.Key())
case reflect.Int32:
keyVal, err = cast.ToInt32E(mapIter.Key())
case reflect.Int64:
keyVal, err = cast.ToInt64E(mapIter.Key())
case reflect.Uint8:
keyVal, err = cast.ToUint8E(mapIter.Key())
case reflect.Uint:
keyVal, err = cast.ToUintE(mapIter.Key())
case reflect.Uint16:
keyVal, err = cast.ToUint16E(mapIter.Key())
case reflect.Uint32:
keyVal, err = cast.ToUint32E(mapIter.Key())
case reflect.Uint64:
keyVal, err = cast.ToUint64E(mapIter.Key())
}
if err != nil {
return err
}
switch valKind {
case reflect.Bool:
valVal, err = cast.ToBoolE(mapIter.Value())
case reflect.String:
valVal, err = cast.ToStringE(mapIter.Value())
case reflect.Float32:
valVal, err = cast.ToFloat32E(mapIter.Value())
case reflect.Float64:
valVal, err = cast.ToFloat64E(mapIter.Value())
case reflect.Int8:
valVal, err = cast.ToInt8E(mapIter.Value())
case reflect.Int:
valVal, err = cast.ToIntE(mapIter.Value())
case reflect.Int16:
valVal, err = cast.ToInt16E(mapIter.Value())
case reflect.Int32:
valVal, err = cast.ToInt32E(mapIter.Value())
case reflect.Int64:
valVal, err = cast.ToInt64E(mapIter.Value())
case reflect.Uint8:
valVal, err = cast.ToUint8E(mapIter.Value())
case reflect.Uint:
valVal, err = cast.ToUintE(mapIter.Value())
case reflect.Uint16:
valVal, err = cast.ToUint16E(mapIter.Value())
case reflect.Uint32:
valVal, err = cast.ToUint32E(mapIter.Value())
case reflect.Uint64:
valVal, err = cast.ToUint64E(mapIter.Value())
}
if err != nil {
return err
}
val.SetMapIndex(reflect.ValueOf(keyVal), reflect.ValueOf(valVal))
}
return nil
}
if l := len(v) % 2; l == 1 {
v = v[:len(v)-1]
}
var (
keyVal interface{}
valVal interface{}
)
for i := 0; i < len(v); i += 2 {
switch keyKind {
case reflect.Bool:
keyVal, err = cast.ToBoolE(v[i])
case reflect.String:
keyVal, err = cast.ToStringE(v[i])
case reflect.Float32:
keyVal, err = cast.ToFloat32E(v[i])
case reflect.Float64:
keyVal, err = cast.ToFloat64E(v[i])
case reflect.Int8:
keyVal, err = cast.ToInt8E(v[i])
case reflect.Int:
keyVal, err = cast.ToIntE(v[i])
case reflect.Int16:
keyVal, err = cast.ToInt16E(v[i])
case reflect.Int32:
keyVal, err = cast.ToInt32E(v[i])
case reflect.Int64:
keyVal, err = cast.ToInt64E(v[i])
case reflect.Uint8:
keyVal, err = cast.ToUint8E(v[i])
case reflect.Uint:
keyVal, err = cast.ToUintE(v[i])
case reflect.Uint16:
keyVal, err = cast.ToUint16E(v[i])
case reflect.Uint32:
keyVal, err = cast.ToUint32E(v[i])
case reflect.Uint64:
keyVal, err = cast.ToUint64E(v[i])
}
if err != nil {
return err
}
switch valKind {
case reflect.Bool:
valVal, err = cast.ToBoolE(v[i+1])
case reflect.String:
valVal, err = cast.ToStringE(v[i+1])
case reflect.Float32:
valVal, err = cast.ToFloat32E(v[i+1])
case reflect.Float64:
valVal, err = cast.ToFloat64E(v[i+1])
case reflect.Int8:
valVal, err = cast.ToInt8E(v[i+1])
case reflect.Int:
valVal, err = cast.ToIntE(v[i+1])
case reflect.Int16:
valVal, err = cast.ToInt16E(v[i+1])
case reflect.Int32:
valVal, err = cast.ToInt32E(v[i+1])
case reflect.Int64:
valVal, err = cast.ToInt64E(v[i+1])
case reflect.Uint8:
valVal, err = cast.ToUint8E(v[i+1])
case reflect.Uint:
valVal, err = cast.ToUintE(v[i+1])
case reflect.Uint16:
valVal, err = cast.ToUint16E(v[i+1])
case reflect.Uint32:
valVal, err = cast.ToUint32E(v[i+1])
case reflect.Uint64:
valVal, err = cast.ToUint64E(v[i+1])
}
if err != nil {
return err
}
val.SetMapIndex(reflect.ValueOf(keyVal), reflect.ValueOf(valVal))
}
default:
dstVal := reflect.ValueOf(dst)
if dstVal.Kind() != reflect.Map {
return nil
}
mapIter := dstVal.MapRange()
for mapIter.Next() {
var (
keyVal interface{}
valVal interface{}
)
switch keyKind {
case reflect.Bool:
keyVal, err = cast.ToBoolE(mapIter.Key())
case reflect.String:
keyVal, err = cast.ToStringE(mapIter.Key())
case reflect.Float32:
keyVal, err = cast.ToFloat32E(mapIter.Key())
case reflect.Float64:
keyVal, err = cast.ToFloat64E(mapIter.Key())
case reflect.Int8:
keyVal, err = cast.ToInt8E(mapIter.Key())
case reflect.Int:
keyVal, err = cast.ToIntE(mapIter.Key())
case reflect.Int16:
keyVal, err = cast.ToInt16E(mapIter.Key())
case reflect.Int32:
keyVal, err = cast.ToInt32E(mapIter.Key())
case reflect.Int64:
keyVal, err = cast.ToInt64E(mapIter.Key())
case reflect.Uint8:
keyVal, err = cast.ToUint8E(mapIter.Key())
case reflect.Uint:
keyVal, err = cast.ToUintE(mapIter.Key())
case reflect.Uint16:
keyVal, err = cast.ToUint16E(mapIter.Key())
case reflect.Uint32:
keyVal, err = cast.ToUint32E(mapIter.Key())
case reflect.Uint64:
keyVal, err = cast.ToUint64E(mapIter.Key())
}
if err != nil {
return err
}
switch valKind {
case reflect.Bool:
valVal, err = cast.ToBoolE(mapIter.Value())
case reflect.String:
valVal, err = cast.ToStringE(mapIter.Value())
case reflect.Float32:
valVal, err = cast.ToFloat32E(mapIter.Value())
case reflect.Float64:
valVal, err = cast.ToFloat64E(mapIter.Value())
case reflect.Int8:
valVal, err = cast.ToInt8E(mapIter.Value())
case reflect.Int:
valVal, err = cast.ToIntE(mapIter.Value())
case reflect.Int16:
valVal, err = cast.ToInt16E(mapIter.Value())
case reflect.Int32:
valVal, err = cast.ToInt32E(mapIter.Value())
case reflect.Int64:
valVal, err = cast.ToInt64E(mapIter.Value())
case reflect.Uint8:
valVal, err = cast.ToUint8E(mapIter.Value())
case reflect.Uint:
valVal, err = cast.ToUintE(mapIter.Value())
case reflect.Uint16:
valVal, err = cast.ToUint16E(mapIter.Value())
case reflect.Uint32:
valVal, err = cast.ToUint32E(mapIter.Value())
case reflect.Uint64:
valVal, err = cast.ToUint64E(mapIter.Value())
}
if err != nil {
return err
}
val.SetMapIndex(reflect.ValueOf(keyVal), reflect.ValueOf(valVal))
}
return nil
}
return nil
}

View File

@@ -7,12 +7,12 @@ import (
"net/http/pprof" "net/http/pprof"
"sync" "sync"
profile "go.unistack.org/micro/v4/profiler" profile "go.unistack.org/micro/v3/profiler"
) )
type httpProfile struct { type httpProfile struct {
server *http.Server server *http.Server
mu sync.Mutex sync.Mutex
running bool running bool
} }
@@ -21,8 +21,8 @@ var DefaultAddress = ":6060"
// Start the profiler // Start the profiler
func (h *httpProfile) Start() error { func (h *httpProfile) Start() error {
h.mu.Lock() h.Lock()
defer h.mu.Unlock() defer h.Unlock()
if h.running { if h.running {
return nil return nil
@@ -30,9 +30,9 @@ func (h *httpProfile) Start() error {
go func() { go func() {
if err := h.server.ListenAndServe(); err != nil { if err := h.server.ListenAndServe(); err != nil {
h.mu.Lock() h.Lock()
h.running = false h.running = false
h.mu.Unlock() h.Unlock()
} }
}() }()
@@ -43,8 +43,8 @@ func (h *httpProfile) Start() error {
// Stop the profiler // Stop the profiler
func (h *httpProfile) Stop() error { func (h *httpProfile) Stop() error {
h.mu.Lock() h.Lock()
defer h.mu.Unlock() defer h.Unlock()
if !h.running { if !h.running {
return nil return nil

View File

@@ -9,7 +9,7 @@ import (
"sync" "sync"
"time" "time"
profile "go.unistack.org/micro/v4/profiler" profile "go.unistack.org/micro/v3/profiler"
) )
type profiler struct { type profiler struct {
@@ -17,7 +17,7 @@ type profiler struct {
cpuFile *os.File cpuFile *os.File
memFile *os.File memFile *os.File
opts profile.Options opts profile.Options
mu sync.Mutex sync.Mutex
running bool running bool
} }
@@ -39,8 +39,8 @@ func (p *profiler) writeHeap(f *os.File) {
} }
func (p *profiler) Start() error { func (p *profiler) Start() error {
p.mu.Lock() p.Lock()
defer p.mu.Unlock() defer p.Unlock()
if p.running { if p.running {
return nil return nil
@@ -86,8 +86,8 @@ func (p *profiler) Start() error {
} }
func (p *profiler) Stop() error { func (p *profiler) Stop() error {
p.mu.Lock() p.Lock()
defer p.mu.Unlock() defer p.Unlock()
select { select {
case <-p.exit: case <-p.exit:

98
proxy/options.go Normal file
View File

@@ -0,0 +1,98 @@
// Package proxy is a transparent proxy built on the micro/server
package proxy
import (
"go.unistack.org/micro/v3/client"
"go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v3/router"
"go.unistack.org/micro/v3/tracer"
)
// Options for proxy
type Options struct {
// Tracer used for tracing
Tracer tracer.Tracer
// Client for communication
Client client.Client
// Router for routing
Router router.Router
// Logger used for logging
Logger logger.Logger
// Meter used for metrics
Meter meter.Meter
// Links holds the communication links
Links map[string]client.Client
// Endpoint holds the destination address
Endpoint string
}
// Option func signature
type Option func(o *Options)
// NewOptions returns new options struct that filled by opts
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
Meter: meter.DefaultMeter,
Tracer: tracer.DefaultTracer,
}
for _, o := range opts {
o(&options)
}
return options
}
// WithEndpoint sets a proxy endpoint
func WithEndpoint(e string) Option {
return func(o *Options) {
o.Endpoint = e
}
}
// WithClient sets the client
func WithClient(c client.Client) Option {
return func(o *Options) {
o.Client = c
}
}
// WithRouter specifies the router to use
func WithRouter(r router.Router) Option {
return func(o *Options) {
o.Router = r
}
}
// WithLogger specifies the logger to use
func WithLogger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// WithMeter specifies the meter to use
func WithMeter(m meter.Meter) Option {
return func(o *Options) {
o.Meter = m
}
}
// WithLink sets a link for outbound requests
func WithLink(name string, c client.Client) Option {
return func(o *Options) {
if o.Links == nil {
o.Links = make(map[string]client.Client)
}
o.Links[name] = c
}
}
// Tracer to be used for tracing
func Tracer(t tracer.Tracer) Option {
return func(o *Options) {
o.Tracer = t
}
}

21
proxy/proxy.go Normal file
View File

@@ -0,0 +1,21 @@
// Package proxy is a transparent proxy built on the micro/server
package proxy
import (
"context"
"go.unistack.org/micro/v3/server"
)
// DefaultEndpoint holds default proxy address
var DefaultEndpoint = "localhost:9090"
// Proxy can be used as a proxy server for micro services
type Proxy interface {
// ProcessMessage handles inbound messages
ProcessMessage(context.Context, server.Message) error
// ServeRequest handles inbound requests
ServeRequest(context.Context, server.Request, server.Response) error
// Name of the proxy protocol
String() string
}

View File

@@ -6,10 +6,9 @@ import (
"sync" "sync"
"time" "time"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/util/id"
"go.unistack.org/micro/v4/util/id"
) )
var ( var (
@@ -26,6 +25,7 @@ type node struct {
type record struct { type record struct {
Name string Name string
Version string Version string
Metadata map[string]string
Nodes map[string]*node Nodes map[string]*node
} }
@@ -33,7 +33,7 @@ type memory struct {
records map[string]services records map[string]services
watchers map[string]*watcher watchers map[string]*watcher
opts register.Options opts register.Options
mu sync.RWMutex sync.RWMutex
} }
// services is a KV map with service name as the key and a map of records as the value // services is a KV map with service name as the key and a map of records as the value
@@ -57,7 +57,7 @@ func (m *memory) ttlPrune() {
defer prune.Stop() defer prune.Stop()
for range prune.C { for range prune.C {
m.mu.Lock() m.Lock()
for namespace, services := range m.records { for namespace, services := range m.records {
for service, versions := range services { for service, versions := range services {
for version, record := range versions { for version, record := range versions {
@@ -72,24 +72,24 @@ func (m *memory) ttlPrune() {
} }
} }
} }
m.mu.Unlock() m.Unlock()
} }
} }
func (m *memory) sendEvent(r *register.Result) { func (m *memory) sendEvent(r *register.Result) {
m.mu.RLock() m.RLock()
watchers := make([]*watcher, 0, len(m.watchers)) watchers := make([]*watcher, 0, len(m.watchers))
for _, w := range m.watchers { for _, w := range m.watchers {
watchers = append(watchers, w) watchers = append(watchers, w)
} }
m.mu.RUnlock() m.RUnlock()
for _, w := range watchers { for _, w := range watchers {
select { select {
case <-w.exit: case <-w.exit:
m.mu.Lock() m.Lock()
delete(m.watchers, w.id) delete(m.watchers, w.id)
m.mu.Unlock() m.Unlock()
default: default:
select { select {
case w.res <- r: case w.res <- r:
@@ -113,8 +113,8 @@ func (m *memory) Init(opts ...register.Option) error {
} }
// add services // add services
m.mu.Lock() m.Lock()
defer m.mu.Unlock() defer m.Unlock()
return nil return nil
} }
@@ -124,8 +124,8 @@ func (m *memory) Options() register.Options {
} }
func (m *memory) Register(_ context.Context, s *register.Service, opts ...register.RegisterOption) error { func (m *memory) Register(_ context.Context, s *register.Service, opts ...register.RegisterOption) error {
m.mu.Lock() m.Lock()
defer m.mu.Unlock() defer m.Unlock()
options := register.NewRegisterOptions(opts...) options := register.NewRegisterOptions(opts...)
@@ -160,14 +160,19 @@ func (m *memory) Register(_ context.Context, s *register.Service, opts ...regist
continue continue
} }
md := metadata.Copy(n.Metadata) metadata := make(map[string]string, len(n.Metadata))
// make copy of metadata
for k, v := range n.Metadata {
metadata[k] = v
}
// add the node // add the node
srvs[s.Name][s.Version].Nodes[n.ID] = &node{ srvs[s.Name][s.Version].Nodes[n.ID] = &node{
Node: &register.Node{ Node: &register.Node{
ID: n.ID, ID: n.ID,
Address: n.Address, Address: n.Address,
Metadata: md, Metadata: metadata,
}, },
TTL: options.TTL, TTL: options.TTL,
LastSeen: time.Now(), LastSeen: time.Now(),
@@ -197,8 +202,8 @@ func (m *memory) Register(_ context.Context, s *register.Service, opts ...regist
} }
func (m *memory) Deregister(ctx context.Context, s *register.Service, opts ...register.DeregisterOption) error { func (m *memory) Deregister(ctx context.Context, s *register.Service, opts ...register.DeregisterOption) error {
m.mu.Lock() m.Lock()
defer m.mu.Unlock() defer m.Unlock()
options := register.NewDeregisterOptions(opts...) options := register.NewDeregisterOptions(opts...)
@@ -264,9 +269,9 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...registe
// if it's a wildcard domain, return from all domains // if it's a wildcard domain, return from all domains
if options.Namespace == register.WildcardNamespace { if options.Namespace == register.WildcardNamespace {
m.mu.RLock() m.RLock()
recs := m.records recs := m.records
m.mu.RUnlock() m.RUnlock()
var services []*register.Service var services []*register.Service
@@ -286,8 +291,8 @@ func (m *memory) LookupService(ctx context.Context, name string, opts ...registe
return services, nil return services, nil
} }
m.mu.RLock() m.RLock()
defer m.mu.RUnlock() defer m.RUnlock()
// check the domain exists // check the domain exists
services, ok := m.records[options.Namespace] services, ok := m.records[options.Namespace]
@@ -319,9 +324,9 @@ func (m *memory) ListServices(ctx context.Context, opts ...register.ListOption)
// if it's a wildcard domain, list from all domains // if it's a wildcard domain, list from all domains
if options.Namespace == register.WildcardNamespace { if options.Namespace == register.WildcardNamespace {
m.mu.RLock() m.RLock()
recs := m.records recs := m.records
m.mu.RUnlock() m.RUnlock()
var services []*register.Service var services []*register.Service
@@ -336,8 +341,8 @@ func (m *memory) ListServices(ctx context.Context, opts ...register.ListOption)
return services, nil return services, nil
} }
m.mu.RLock() m.RLock()
defer m.mu.RUnlock() defer m.RUnlock()
// ensure the domain exists // ensure the domain exists
services, ok := m.records[options.Namespace] services, ok := m.records[options.Namespace]
@@ -371,9 +376,9 @@ func (m *memory) Watch(ctx context.Context, opts ...register.WatchOption) (regis
wo: wo, wo: wo,
} }
m.mu.Lock() m.Lock()
m.watchers[w.id] = w m.watchers[w.id] = w
m.mu.Unlock() m.Unlock()
return w, nil return w, nil
} }
@@ -447,15 +452,23 @@ func serviceToRecord(s *register.Service, ttl time.Duration) *record {
} }
func recordToService(r *record, namespace string) *register.Service { func recordToService(r *record, namespace string) *register.Service {
metadata := make(map[string]string, len(r.Metadata))
for k, v := range r.Metadata {
metadata[k] = v
}
nodes := make([]*register.Node, len(r.Nodes)) nodes := make([]*register.Node, len(r.Nodes))
i := 0 i := 0
for _, n := range r.Nodes { for _, n := range r.Nodes {
nmd := metadata.Copy(n.Metadata) md := make(map[string]string, len(n.Metadata))
for k, v := range n.Metadata {
md[k] = v
}
nodes[i] = &register.Node{ nodes[i] = &register.Node{
ID: n.ID, ID: n.ID,
Address: n.Address, Address: n.Address,
Metadata: nmd, Metadata: md,
} }
i++ i++
} }

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/register"
) )
var testData = map[string][]*register.Service{ var testData = map[string][]*register.Service{

View File

@@ -5,10 +5,10 @@ import (
"crypto/tls" "crypto/tls"
"time" "time"
"go.unistack.org/micro/v4/codec" "go.unistack.org/micro/v3/codec"
"go.unistack.org/micro/v4/logger" "go.unistack.org/micro/v3/logger"
"go.unistack.org/micro/v4/meter" "go.unistack.org/micro/v3/meter"
"go.unistack.org/micro/v4/tracer" "go.unistack.org/micro/v3/tracer"
) )
// Options holds options for register // Options holds options for register

View File

@@ -5,7 +5,7 @@ import (
"context" "context"
"errors" "errors"
"go.unistack.org/micro/v4/metadata" "go.unistack.org/micro/v3/metadata"
) )
const ( const (
@@ -69,7 +69,6 @@ type Service struct {
type Node struct { type Node struct {
Metadata metadata.Metadata `json:"metadata,omitempty"` Metadata metadata.Metadata `json:"metadata,omitempty"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
// Address also prefixed with scheme like grpc://xx.xx.xx.xx:1234
Address string `json:"address,omitempty"` Address string `json:"address,omitempty"`
} }

View File

@@ -7,7 +7,7 @@ import (
"sync" "sync"
"time" "time"
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
) )
// Resolver is a DNS network resolve // Resolver is a DNS network resolve

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
) )
// Resolver is a DNS network resolve // Resolver is a DNS network resolve

View File

@@ -8,7 +8,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
) )
// nolint: golint,revive // nolint: golint,revive

View File

@@ -2,7 +2,7 @@
package noop package noop
import ( import (
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
) )
// Resolver contains noop resolver // Resolver contains noop resolver

View File

@@ -4,8 +4,8 @@ package register
import ( import (
"context" "context"
"go.unistack.org/micro/v4/register" "go.unistack.org/micro/v3/register"
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
) )
// Resolver is a register network resolver // Resolver is a register network resolver

View File

@@ -2,7 +2,7 @@
package static package static
import ( import (
"go.unistack.org/micro/v4/resolver" "go.unistack.org/micro/v3/resolver"
) )
// Resolver returns a static list of nodes. In the event the node list // Resolver returns a static list of nodes. In the event the node list

Some files were not shown because too many files have changed in this diff Show More