Compare commits

...

115 Commits

Author SHA1 Message Date
Asim Aslam
d2fdbcc742 Update go modules 2019-02-13 13:32:55 +00:00
Asim Aslam
0cdae40f04 Merge pull request #416 from jiyeyuran/patch-4
reuse rcache
2019-02-13 09:58:49 +00:00
xinfei.wu
a56929d1b8 reuse rcache 2019-02-13 17:47:31 +08:00
Asim Aslam
c9bcdc8438 Merge pull request #415 from unistack-org/rejoin
registry: gossip add Reconnect and Timeout
2019-02-12 14:37:45 +00:00
36532c94b2 registry: [gossip] add ConnectRetry and ConnectTimeout
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2019-02-12 17:16:35 +03:00
Asim Aslam
3580cd1b1e no sponsors 2019-02-11 18:37:40 +00:00
Asim Aslam
a3ecd36763 add ability to set address 2019-02-11 18:37:25 +00:00
Asim Aslam
78b7ee9078 update readme 2019-02-09 12:25:34 +00:00
Asim Aslam
82bcb8748e update go modules 2019-02-07 12:42:45 +00:00
Asim Aslam
31fc8df2ba add server request body 2019-02-04 13:13:03 +00:00
Asim Aslam
baf7de76bf Merge branch 'master' of github.com:micro/go-micro 2019-02-04 10:29:26 +00:00
Asim Aslam
31b6cad47b make copy before writing 2019-02-04 10:29:10 +00:00
Asim Aslam
686171c26d Merge pull request #413 from qkzsky/qkzsky-rpc-fix
client close: rpc: unable to write error response
2019-02-03 13:13:10 +00:00
kuangzhiqiang
6be205fd40 client close: rpc: unable to write error response
when client close notice: "rpc: unable to write error response..."
2019-02-03 19:12:13 +08:00
Asim Aslam
89014160fc Merge pull request #411 from unistack-org/gossip
registry: gossip unify registry option passing, optimize
2019-02-01 22:21:08 +00:00
422e2002a0 registry: gossip unify registry option passing, optimize
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2019-02-02 01:12:39 +03:00
Asim Aslam
cead99ac44 fix nil pointer 2019-02-01 16:01:51 +00:00
Asim Aslam
c03d935ffd fallback for 0.14.0 and older 2019-02-01 15:57:34 +00:00
Asim Aslam
88e12347d0 update mdns to remove race condition 2019-02-01 13:41:11 +00:00
Asim Aslam
652b1067f5 fix data race 2019-02-01 09:05:03 +00:00
Asim Aslam
7888d3e13d use official h2c server 2019-01-31 17:14:36 +00:00
Asim Aslam
b1a31134bd Support micro proxy 2019-01-30 18:42:11 +00:00
Asim Aslam
107b571019 Add go mod 2019-01-30 11:43:40 +00:00
Asim Aslam
89c8e1f4a7 update readme 2019-01-29 09:20:34 +00:00
Asim Aslam
a06cd72337 update image 2019-01-29 09:08:14 +00:00
Asim Aslam
e22fa01935 fix ticker 2019-01-24 16:08:04 +00:00
Asim Aslam
a5015692e3 Merge pull request #400 from micro/interval
Move RegisterInterval into the server
2019-01-24 13:55:05 +00:00
Asim Aslam
539b8c1a3b Move RegisterInterval into the server 2019-01-24 13:22:17 +00:00
Asim Aslam
67a738b504 Merge pull request #399 from unistack-org/master
add context to SubscriberOptions
2019-01-24 13:11:33 +00:00
ac1afea7fc add context to server.SubscriberOptions and broker.SubscribeOption
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2019-01-24 15:36:01 +03:00
Asim Aslam
8090f9968d Update headers to remove X- prefix 2019-01-24 10:11:02 +00:00
Asim Aslam
7542aafd29 Update package comment 2019-01-23 18:15:17 +00:00
Asim Aslam
13de868b21 Rename 2019-01-23 18:14:36 +00:00
Asim Aslam
d090a97a3d Merge pull request #396 from micro/error
Fix #394 invalid error handling in rpc_router ServeRequest
2019-01-22 14:28:41 +00:00
Asim Aslam
8a0d5f0489 log if we can't even respond 2019-01-22 13:55:04 +00:00
Asim Aslam
2ed676acf4 handle errors differently 2019-01-22 13:52:18 +00:00
Asim Aslam
d8ba18deff change logging 2019-01-22 12:18:33 +00:00
Asim Aslam
1321782785 in case of reload return nil 2019-01-19 10:20:16 +00:00
Asim Aslam
48b80dd051 replace memory registry 2019-01-18 17:29:17 +00:00
Asim Aslam
943219f203 Merge pull request #393 from micro/legacy
Evolution of Codecs and Methods
2019-01-18 12:40:05 +00:00
Asim Aslam
6468733d98 Use protocol from node metadata 2019-01-18 12:30:39 +00:00
Asim Aslam
9bd32645be Account for old target 2019-01-18 10:43:41 +00:00
Asim Aslam
f41be53ff8 Add ability to process legacy requests 2019-01-18 10:23:36 +00:00
Asim Aslam
2cd2258731 For the legacy 2019-01-18 10:12:57 +00:00
Asim Aslam
9ce9977d21 Don't read unless we have b 2019-01-17 12:09:04 +00:00
Asim Aslam
617db003d4 Copy metadata 2019-01-17 09:40:49 +00:00
Asim Aslam
7b89b36e37 add benchmarks 2019-01-16 18:54:43 +00:00
Asim Aslam
e2e426b90c Increase default pool size 2019-01-16 18:54:32 +00:00
Asim Aslam
5b95ce7f26 Silence broker during tests 2019-01-16 18:54:04 +00:00
Asim Aslam
082f57fcad We can just check nil vals 2019-01-16 15:42:42 +00:00
Asim Aslam
cc5629fb6b Don't return zero length services 2019-01-16 15:41:37 +00:00
Asim Aslam
784a89b488 Allow bytes.Frame to be set to sent just bytes 2019-01-16 15:27:57 +00:00
Asim Aslam
a9c0b95603 update readme 2019-01-16 13:12:21 +00:00
Asim Aslam
7bd0bd14c8 Merge pull request #386 from micro/mdns
Set MDNS as default registry
2019-01-15 16:59:07 +00:00
Asim Aslam
7314af347b Set MDNS as default registry 2019-01-15 16:50:37 +00:00
Asim Aslam
00661f8a99 Clarify log message 2019-01-15 15:17:30 +00:00
Asim Aslam
e362466e8a use default router 2019-01-14 21:45:43 +00:00
Asim Aslam
c1d0237370 Add client response 2019-01-14 21:30:43 +00:00
Asim Aslam
f2ac73eae5 only log error if its plus 3 2019-01-14 16:09:51 +00:00
Asim Aslam
39c24baca9 rename mock things to memory 2019-01-14 15:27:25 +00:00
Asim Aslam
c17d0fcc0f grpc request 2019-01-13 19:54:07 +00:00
Asim Aslam
e1bc240a14 Respond with error type 2019-01-13 12:15:35 +00:00
Asim Aslam
01f6683035 Add router option 2019-01-13 12:15:13 +00:00
Asim Aslam
bfd341a269 Execute wrappers before router 2019-01-11 15:49:54 +00:00
Asim Aslam
9897c630ae remove request/response 2019-01-11 14:04:37 +00:00
Asim Aslam
36788487a7 set headers as appropriate 2019-01-11 13:44:47 +00:00
Asim Aslam
3043841cf5 Don't process nil 2019-01-10 22:14:32 +00:00
Asim Aslam
04103fe048 Merge pull request #379 from micro/endpoint
Rename method to endpoint
2019-01-10 22:12:28 +00:00
Asim Aslam
9adebfcf1e rename method to endpoint 2019-01-10 21:25:31 +00:00
Asim Aslam
f853f88bcd gofmt 2019-01-10 20:35:20 +00:00
Asim Aslam
40ff5b749b Set topic header 2019-01-10 20:35:10 +00:00
Asim Aslam
59d82b0abe Add response 2019-01-10 11:43:36 +00:00
Asim Aslam
648da5494f Change a few things 2019-01-10 11:39:39 +00:00
Asim Aslam
bb31480f1a downgrade code generated stuff 2019-01-10 10:57:04 +00:00
Asim Aslam
c086c33bb3 remove codecs 2019-01-10 09:42:02 +00:00
Asim Aslam
6e0e4a684c Further crufting 2019-01-09 19:28:13 +00:00
Asim Aslam
873fc6d663 rewriting a lot 2019-01-09 19:11:47 +00:00
Asim Aslam
1561ccbc14 remove clientCodec 2019-01-09 17:33:28 +00:00
Asim Aslam
d004c9624b Add router modifications 2019-01-09 16:20:57 +00:00
Asim Aslam
ee380c6b7a reorder 2019-01-09 09:06:30 +00:00
Asim Aslam
7a1f735825 remove server codec 2019-01-09 09:02:30 +00:00
Asim Aslam
69119cc622 Merge pull request #376 from jiyeyuran/patch-3
add locker
2019-01-09 08:42:08 +00:00
xinfei.wu
eec1726f1d add package comment 2019-01-09 16:31:23 +08:00
xinfei.wu
453ce2fcbe add locker 2019-01-09 14:24:12 +08:00
Asim Aslam
d5df31eeb8 Merge pull request #375 from micro/codec
further codec changes
2019-01-08 21:04:22 +00:00
Asim Aslam
f46828be33 Add Router interface 2019-01-08 20:32:47 +00:00
Asim Aslam
4cb41721f1 further codec changes 2019-01-08 15:38:25 +00:00
Asim Aslam
216dbb771a rename requestHeader 2019-01-07 18:25:31 +00:00
Asim Aslam
c9963cb870 rename 2019-01-07 18:20:47 +00:00
Asim Aslam
e8b431c5ff rename codec interface 2019-01-07 18:17:13 +00:00
Asim Aslam
9544058af3 Merge pull request #372 from micro/codec
Switch default codec and add default codec for server
2019-01-07 17:54:28 +00:00
Asim Aslam
c717af21ac Some router changes 2019-01-07 17:17:06 +00:00
Asim Aslam
46ece968d4 rename service to router 2019-01-07 14:44:40 +00:00
Asim Aslam
fcc730931c Merge pull request #371 from micro/dns
Add dns selector
2019-01-07 13:56:24 +00:00
Asim Aslam
d519180806 Merge branch 'master' into dns 2019-01-07 13:52:37 +00:00
Asim Aslam
78af321790 Merge pull request #367 from micro/static
Add static selector
2019-01-07 13:51:47 +00:00
Asim Aslam
d179c971af Switch default codec and add default codec for server 2019-01-07 13:48:38 +00:00
Asim Aslam
d6a5ff432c add net.LookupHost for dns 2019-01-07 09:34:07 +00:00
Asim Aslam
5aeb28dfee Add error header 2019-01-07 09:11:36 +00:00
Asim Aslam
f9da55e8a9 Add dns selector 2019-01-07 07:41:26 +00:00
Asim Aslam
4692af4393 Add static selector 2019-01-06 21:12:02 +00:00
Asim Aslam
4adc31e62d add bytes codec, still unused 2019-01-04 14:07:16 +00:00
Asim Aslam
461df8d464 Merge pull request #364 from micro/inbox
Add inbox feature to http broker
2019-01-03 11:27:46 +00:00
Asim Aslam
7c2cbe2ad2 better error handling 2019-01-03 11:23:06 +00:00
Asim Aslam
abbeb6d068 add inbox feature to http broker 2019-01-02 19:27:46 +00:00
Asim Aslam
ce36d0156d Merge pull request #362 from micro/codec
Make json/protobuf/grpc codecs
2019-01-02 18:01:34 +00:00
Asim Aslam
29ef3676b2 Merge pull request #363 from micro/proxy
Add support for http proxy
2019-01-02 15:28:57 +00:00
Asim Aslam
2761b8e0f5 Add support for http proxy 2019-01-02 15:24:17 +00:00
Asim Aslam
ed580204a8 Add grpc codec 2019-01-02 12:55:06 +00:00
Asim Aslam
7cf94162b8 remove fmt comment 2019-01-02 12:50:25 +00:00
Asim Aslam
e2623d8ef5 Make json/protobuf codecs 2018-12-31 22:01:16 +00:00
Asim Aslam
b3b4bc6059 remove Plus 2018-12-31 20:51:22 +00:00
Asim Aslam
386ced576a Process header/body in one call 2018-12-31 17:53:16 +00:00
Asim Aslam
dcf7a56f9b rename codec 2018-12-31 17:28:19 +00:00
Asim Aslam
460fb3e70c update package comments 2018-12-29 16:18:05 +00:00
108 changed files with 4724 additions and 2698 deletions

View File

@@ -1,6 +1,6 @@
# Go Micro [![License](https://img.shields.io/:license-apache-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GoDoc](https://godoc.org/github.com/micro/go-micro?status.svg)](https://godoc.org/github.com/micro/go-micro) [![Travis CI](https://api.travis-ci.org/micro/go-micro.svg?branch=master)](https://travis-ci.org/micro/go-micro) [![Go Report Card](https://goreportcard.com/badge/micro/go-micro)](https://goreportcard.com/report/github.com/micro/go-micro)
Go Micro is a pluggable framework for micro service development.
Go Micro is a framework for micro service development.
## Overview
@@ -8,7 +8,7 @@ Go Micro provides the core requirements for distributed systems development incl
The **micro** philosophy is sane defaults with a pluggable architecture. We provide defaults to get you started quickly
but everything can be easily swapped out.
<img src="https://micro.mu/docs/images/go-micro.png" />
<img src="https://micro.mu/docs/images/go-micro.svg" />
Plugins are available at [github.com/micro/go-plugins](https://github.com/micro/go-plugins).
@@ -19,8 +19,9 @@ Follow us on [Twitter](https://twitter.com/microhq) or join the [Slack](http://s
Go Micro abstracts away the details of distributed systems. Here are the main features.
- **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. Consul is the default discovery
system with multicast DNS (mdns) as a local option or the SWIM protocol (gossip) for zero dependency p2p networks.
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system. You can optionally set gossip using the SWIM protocol for p2p networks or consul for a
resilient cloud-native setup.
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
@@ -30,7 +31,7 @@ across the services and retry a different node if there's a problem.
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
and server handle this by default. This includes protobuf and json by default.
- **Sync Streaming** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
- **Request/Response** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed. The default
transport is http/1.1 or http2 when tls is enabled.
@@ -51,5 +52,3 @@ See the [docs](https://micro.mu/docs/go-micro.html) for detailed information on
Sixt is an Enterprise Sponsor of Micro
<a href="https://micro.mu/blog/2016/04/25/announcing-sixt-sponsorship.html"><img src="https://micro.mu/sixt_logo.png" width=150px height="auto" /></a>
Become a sponsor by backing micro on [Patreon](https://www.patreon.com/microhq)

View File

@@ -1,10 +0,0 @@
package codec
// Codec is used for encoding where the broker doesn't natively support
// headers in the message type. In this case the entire message is
// encoded as the payload
type Codec interface {
Marshal(interface{}) ([]byte, error)
Unmarshal([]byte, interface{}) error
String() string
}

View File

@@ -1,25 +0,0 @@
package json
import (
"encoding/json"
"github.com/micro/go-micro/broker/codec"
)
type jsonCodec struct{}
func (j jsonCodec) Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (j jsonCodec) Unmarshal(d []byte, v interface{}) error {
return json.Unmarshal(d, v)
}
func (j jsonCodec) String() string {
return "json"
}
func NewCodec() codec.Codec {
return jsonCodec{}
}

View File

@@ -1,35 +0,0 @@
package noop
import (
"errors"
"github.com/micro/go-micro/broker"
"github.com/micro/go-micro/broker/codec"
)
type noopCodec struct{}
func (n noopCodec) Marshal(v interface{}) ([]byte, error) {
msg, ok := v.(*broker.Message)
if !ok {
return nil, errors.New("invalid message")
}
return msg.Body, nil
}
func (n noopCodec) Unmarshal(d []byte, v interface{}) error {
msg, ok := v.(*broker.Message)
if !ok {
return errors.New("invalid message")
}
msg.Body = d
return nil
}
func (n noopCodec) String() string {
return "noop"
}
func NewCodec() codec.Codec {
return noopCodec{}
}

View File

@@ -19,8 +19,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/micro/go-log"
"github.com/micro/go-micro/broker/codec/json"
"github.com/micro/go-micro/codec/json"
merr "github.com/micro/go-micro/errors"
"github.com/micro/go-micro/registry"
"github.com/micro/go-rcache"
@@ -45,6 +44,10 @@ type httpBroker struct {
subscribers map[string][]*httpSubscriber
running bool
exit chan chan error
// offline message inbox
mtx sync.RWMutex
inbox map[string][][]byte
}
type httpSubscriber struct {
@@ -104,7 +107,7 @@ func newTransport(config *tls.Config) *http.Transport {
func newHttpBroker(opts ...Option) Broker {
options := Options{
Codec: json.NewCodec(),
Codec: json.Marshaler{},
Context: context.TODO(),
}
@@ -133,6 +136,7 @@ func newHttpBroker(opts ...Option) Broker {
subscribers: make(map[string][]*httpSubscriber),
exit: make(chan chan error),
mux: http.NewServeMux(),
inbox: make(map[string][][]byte),
}
// specify the message handler
@@ -175,6 +179,49 @@ func (h *httpSubscriber) Unsubscribe() error {
return h.hb.unsubscribe(h)
}
func (h *httpBroker) saveMessage(topic string, msg []byte) {
h.mtx.Lock()
defer h.mtx.Unlock()
// get messages
c := h.inbox[topic]
// save message
c = append(c, msg)
// max length 64
if len(c) > 64 {
c = c[:64]
}
// save inbox
h.inbox[topic] = c
}
func (h *httpBroker) getMessage(topic string, num int) [][]byte {
h.mtx.Lock()
defer h.mtx.Unlock()
// get messages
c, ok := h.inbox[topic]
if !ok {
return nil
}
// more message than requests
if len(c) >= num {
msg := c[:num]
h.inbox[topic] = c[num:]
return msg
}
// reset inbox
h.inbox[topic] = nil
// return all messages
return c
}
func (h *httpBroker) subscribe(s *httpSubscriber) error {
h.Lock()
defer h.Unlock()
@@ -349,7 +396,6 @@ func (h *httpBroker) Connect() error {
return err
}
log.Logf("Broker Listening on %s", l.Addr().String())
addr := h.address
h.address = l.Addr().String()
@@ -454,14 +500,7 @@ func (h *httpBroker) Options() Options {
}
func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) error {
h.RLock()
s, err := h.r.GetService("topic:" + topic)
if err != nil {
h.RUnlock()
return err
}
h.RUnlock()
// create the message first
m := &Message{
Header: make(map[string]string),
Body: msg.Body,
@@ -473,12 +512,26 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
m.Header[":topic"] = topic
// encode the message
b, err := h.opts.Codec.Marshal(m)
if err != nil {
return err
}
pub := func(node *registry.Node, b []byte) {
// save the message
h.saveMessage(topic, b)
// now attempt to get the service
h.RLock()
s, err := h.r.GetService("topic:" + topic)
if err != nil {
h.RUnlock()
// ignore error
return nil
}
h.RUnlock()
pub := func(node *registry.Node, t string, b []byte) error {
scheme := "http"
// check if secure is added in metadata
@@ -491,34 +544,71 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption)
uri := fmt.Sprintf("%s://%s:%d%s?%s", scheme, node.Address, node.Port, DefaultSubPath, vals.Encode())
r, err := h.c.Post(uri, "application/json", bytes.NewReader(b))
if err == nil {
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
if err != nil {
return err
}
// discard response body
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
return nil
}
for _, service := range s {
// only process if we have nodes
if len(service.Nodes) == 0 {
continue
}
switch service.Version {
// broadcast version means broadcast to all nodes
case broadcastVersion:
for _, node := range service.Nodes {
// publish async
go pub(node, b)
srv := func(s []*registry.Service, b []byte) {
for _, service := range s {
// only process if we have nodes
if len(service.Nodes) == 0 {
continue
}
default:
// select node to publish to
node := service.Nodes[rand.Int()%len(service.Nodes)]
// publish async
go pub(node, b)
switch service.Version {
// broadcast version means broadcast to all nodes
case broadcastVersion:
var success bool
// publish to all nodes
for _, node := range service.Nodes {
// publish async
if err := pub(node, topic, b); err == nil {
success = true
}
}
// save if it failed to publish at least once
if !success {
h.saveMessage(topic, b)
}
default:
// select node to publish to
node := service.Nodes[rand.Int()%len(service.Nodes)]
// publish async to one node
if err := pub(node, topic, b); err != nil {
// if failed save it
h.saveMessage(topic, b)
}
}
}
}
// do the rest async
go func() {
// get a third of the backlog
messages := h.getMessage(topic, 8)
delay := (len(messages) > 1)
// publish all the messages
for _, msg := range messages {
// serialize here
srv(s, msg)
// sending a backlog of messages
if delay {
time.Sleep(time.Millisecond * 100)
}
}
}()
return nil
}

View File

@@ -5,13 +5,26 @@ import (
"testing"
"time"
glog "github.com/go-log/log"
"github.com/google/uuid"
"github.com/micro/go-micro/registry/mock"
"github.com/micro/go-log"
"github.com/micro/go-micro/registry/memory"
)
func newTestRegistry() *memory.Registry {
r := memory.NewRegistry()
m := r.(*memory.Registry)
m.Setup()
return m
}
func sub(be *testing.B, c int) {
// set no op logger
log.SetLogger(glog.DefaultLogger)
be.StopTimer()
m := mock.NewRegistry()
m := newTestRegistry()
b := NewBroker(Registry(m))
topic := uuid.New().String()
@@ -69,8 +82,11 @@ func sub(be *testing.B, c int) {
}
func pub(be *testing.B, c int) {
// set no op logger
log.SetLogger(glog.DefaultLogger)
be.StopTimer()
m := mock.NewRegistry()
m := newTestRegistry()
b := NewBroker(Registry(m))
topic := uuid.New().String()
@@ -139,7 +155,7 @@ func pub(be *testing.B, c int) {
}
func TestBroker(t *testing.T) {
m := mock.NewRegistry()
m := newTestRegistry()
b := NewBroker(Registry(m))
if err := b.Init(); err != nil {
@@ -186,7 +202,7 @@ func TestBroker(t *testing.T) {
}
func TestConcurrentSubBroker(t *testing.T) {
m := mock.NewRegistry()
m := newTestRegistry()
b := NewBroker(Registry(m))
if err := b.Init(); err != nil {
@@ -243,7 +259,7 @@ func TestConcurrentSubBroker(t *testing.T) {
}
func TestConcurrentPubBroker(t *testing.T) {
m := mock.NewRegistry()
m := newTestRegistry()
b := NewBroker(Registry(m))
if err := b.Init(); err != nil {

View File

@@ -1,5 +1,5 @@
// Package mock provides a mock broker for testing
package mock
// Package memory provides a memory broker
package memory
import (
"errors"
@@ -9,20 +9,20 @@ import (
"github.com/micro/go-micro/broker"
)
type mockBroker struct {
type memoryBroker struct {
opts broker.Options
sync.RWMutex
connected bool
Subscribers map[string][]*mockSubscriber
Subscribers map[string][]*memorySubscriber
}
type mockPublication struct {
type memoryPublication struct {
topic string
message *broker.Message
}
type mockSubscriber struct {
type memorySubscriber struct {
id string
topic string
exit chan bool
@@ -30,15 +30,15 @@ type mockSubscriber struct {
opts broker.SubscribeOptions
}
func (m *mockBroker) Options() broker.Options {
func (m *memoryBroker) Options() broker.Options {
return m.opts
}
func (m *mockBroker) Address() string {
func (m *memoryBroker) Address() string {
return ""
}
func (m *mockBroker) Connect() error {
func (m *memoryBroker) Connect() error {
m.Lock()
defer m.Unlock()
@@ -51,7 +51,7 @@ func (m *mockBroker) Connect() error {
return nil
}
func (m *mockBroker) Disconnect() error {
func (m *memoryBroker) Disconnect() error {
m.Lock()
defer m.Unlock()
@@ -64,14 +64,14 @@ func (m *mockBroker) Disconnect() error {
return nil
}
func (m *mockBroker) Init(opts ...broker.Option) error {
func (m *memoryBroker) Init(opts ...broker.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *mockBroker) Publish(topic string, message *broker.Message, opts ...broker.PublishOption) error {
func (m *memoryBroker) Publish(topic string, message *broker.Message, opts ...broker.PublishOption) error {
m.Lock()
defer m.Unlock()
@@ -84,7 +84,7 @@ func (m *mockBroker) Publish(topic string, message *broker.Message, opts ...brok
return nil
}
p := &mockPublication{
p := &memoryPublication{
topic: topic,
message: message,
}
@@ -98,7 +98,7 @@ func (m *mockBroker) Publish(topic string, message *broker.Message, opts ...brok
return nil
}
func (m *mockBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
func (m *memoryBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
m.Lock()
defer m.Unlock()
@@ -111,7 +111,7 @@ func (m *mockBroker) Subscribe(topic string, handler broker.Handler, opts ...bro
o(&options)
}
sub := &mockSubscriber{
sub := &memorySubscriber{
exit: make(chan bool, 1),
id: uuid.New().String(),
topic: topic,
@@ -124,7 +124,7 @@ func (m *mockBroker) Subscribe(topic string, handler broker.Handler, opts ...bro
go func() {
<-sub.exit
m.Lock()
var newSubscribers []*mockSubscriber
var newSubscribers []*memorySubscriber
for _, sb := range m.Subscribers[topic] {
if sb.id == sub.id {
continue
@@ -138,31 +138,31 @@ func (m *mockBroker) Subscribe(topic string, handler broker.Handler, opts ...bro
return sub, nil
}
func (m *mockBroker) String() string {
return "mock"
func (m *memoryBroker) String() string {
return "memory"
}
func (m *mockPublication) Topic() string {
func (m *memoryPublication) Topic() string {
return m.topic
}
func (m *mockPublication) Message() *broker.Message {
func (m *memoryPublication) Message() *broker.Message {
return m.message
}
func (m *mockPublication) Ack() error {
func (m *memoryPublication) Ack() error {
return nil
}
func (m *mockSubscriber) Options() broker.SubscribeOptions {
func (m *memorySubscriber) Options() broker.SubscribeOptions {
return m.opts
}
func (m *mockSubscriber) Topic() string {
func (m *memorySubscriber) Topic() string {
return m.topic
}
func (m *mockSubscriber) Unsubscribe() error {
func (m *memorySubscriber) Unsubscribe() error {
m.exit <- true
return nil
}
@@ -173,8 +173,8 @@ func NewBroker(opts ...broker.Option) broker.Broker {
o(&options)
}
return &mockBroker{
return &memoryBroker{
opts: options,
Subscribers: make(map[string][]*mockSubscriber),
Subscribers: make(map[string][]*memorySubscriber),
}
}

View File

@@ -1,4 +1,4 @@
package mock
package memory
import (
"fmt"
@@ -7,7 +7,7 @@ import (
"github.com/micro/go-micro/broker"
)
func TestBroker(t *testing.T) {
func TestMemoryBroker(t *testing.T) {
b := NewBroker()
if err := b.Connect(); err != nil {

View File

@@ -4,14 +4,14 @@ import (
"context"
"crypto/tls"
"github.com/micro/go-micro/broker/codec"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/registry"
)
type Options struct {
Addrs []string
Secure bool
Codec codec.Codec
Codec codec.Marshaler
TLSConfig *tls.Config
// Other options for implementations of the interface
// can be stored in a context
@@ -71,7 +71,7 @@ func Addrs(addrs ...string) Option {
// Codec sets the codec used for encoding/decoding used where
// a broker does not support headers
func Codec(c codec.Codec) Option {
func Codec(c codec.Marshaler) Option {
return func(o *Options) {
o.Codec = c
}
@@ -111,3 +111,10 @@ func TLSConfig(t *tls.Config) Option {
o.TLSConfig = t
}
}
// SubscribeContext set context
func SubscribeContext(ctx context.Context) SubscribeOption {
return func(o *SubscribeOptions) {
o.Context = ctx
}
}

View File

@@ -4,6 +4,8 @@ package client
import (
"context"
"time"
"github.com/micro/go-micro/codec"
)
// Client is the interface used to make requests to services.
@@ -13,13 +15,18 @@ type Client interface {
Init(...Option) error
Options() Options
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
NewRequest(service, method string, req interface{}, reqOpts ...RequestOption) Request
NewRequest(service, endpoint string, req interface{}, reqOpts ...RequestOption) Request
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
String() string
}
// Router manages request routing
type Router interface {
SendRequest(context.Context, Request) (Response, error)
}
// Message is the interface for publishing asynchronously
type Message interface {
Topic() string
@@ -29,21 +36,47 @@ type Message interface {
// Request is the interface for a synchronous request used by Call or Stream
type Request interface {
// The service to call
Service() string
// The action to take
Method() string
// The endpoint to invoke
Endpoint() string
// The content type
ContentType() string
Request() interface{}
// The unencoded request body
Body() interface{}
// Write to the encoded request writer. This is nil before a call is made
Codec() codec.Writer
// indicates whether the request will be a streaming one rather than unary
Stream() bool
}
// Response is the response received from a service
type Response interface {
// Read the response
Codec() codec.Reader
// read the header
Header() map[string]string
// Read the undecoded response
Read() ([]byte, error)
}
// Stream is the inteface for a bidirectional synchronous stream
type Stream interface {
// Context for the stream
Context() context.Context
// The request made
Request() Request
// The response read
Response() Response
// Send will encode and send a request
Send(interface{}) error
// Recv will decode and read a response
Recv(interface{}) error
// Error returns the stream error
Error() error
// Close closes the stream
Close() error
}
@@ -74,7 +107,7 @@ var (
// DefaultRequestTimeout is the default request timeout
DefaultRequestTimeout = time.Second * 5
// DefaultPoolSize sets the connection pool size
DefaultPoolSize = 1
DefaultPoolSize = 100
// DefaultPoolTTL sets the connection pool ttl
DefaultPoolTTL = time.Minute
)
@@ -102,8 +135,8 @@ func NewClient(opt ...Option) Client {
// Creates a new request using the default client. Content Type will
// be set to the default within options and use the appropriate codec
func NewRequest(service, method string, request interface{}, reqOpts ...RequestOption) Request {
return DefaultClient.NewRequest(service, method, request, reqOpts...)
func NewRequest(service, endpoint string, request interface{}, reqOpts ...RequestOption) Request {
return DefaultClient.NewRequest(service, endpoint, request, reqOpts...)
}
// Creates a streaming connection with a service and returns responses on the

View File

@@ -16,7 +16,7 @@ var (
)
type MockResponse struct {
Method string
Endpoint string
Response interface{}
Error error
}
@@ -54,8 +54,8 @@ func (m *MockClient) NewMessage(topic string, msg interface{}, opts ...client.Me
return m.Client.NewMessage(topic, msg, opts...)
}
func (m *MockClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
return m.Client.NewRequest(service, method, req, reqOpts...)
func (m *MockClient) NewRequest(service, endpoint string, req interface{}, reqOpts ...client.RequestOption) client.Request {
return m.Client.NewRequest(service, endpoint, req, reqOpts...)
}
func (m *MockClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
@@ -68,7 +68,7 @@ func (m *MockClient) Call(ctx context.Context, req client.Request, rsp interface
}
for _, r := range response {
if r.Method != req.Method() {
if r.Endpoint != req.Endpoint() {
continue
}
@@ -91,7 +91,7 @@ func (m *MockClient) Call(ctx context.Context, req client.Request, rsp interface
return nil
}
return fmt.Errorf("rpc: can't find service %s", req.Method())
return fmt.Errorf("rpc: can't find service %s", req.Endpoint())
}
func (m *MockClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {

View File

@@ -13,17 +13,17 @@ func TestClient(t *testing.T) {
}
response := []MockResponse{
{Method: "Foo.Bar", Response: map[string]interface{}{"foo": "bar"}},
{Method: "Foo.Struct", Response: &TestResponse{Param: "aparam"}},
{Method: "Foo.Fail", Error: errors.InternalServerError("go.mock", "failed")},
{Method: "Foo.Func", Response: func() string { return "string" }},
{Method: "Foo.FuncStruct", Response: func() *TestResponse { return &TestResponse{Param: "aparam"} }},
{Endpoint: "Foo.Bar", Response: map[string]interface{}{"foo": "bar"}},
{Endpoint: "Foo.Struct", Response: &TestResponse{Param: "aparam"}},
{Endpoint: "Foo.Fail", Error: errors.InternalServerError("go.mock", "failed")},
{Endpoint: "Foo.Func", Response: func() string { return "string" }},
{Endpoint: "Foo.FuncStruct", Response: func() *TestResponse { return &TestResponse{Param: "aparam"} }},
}
c := NewClient(Response("go.mock", response))
for _, r := range response {
req := c.NewRequest("go.mock", r.Method, map[string]interface{}{"foo": "bar"})
req := c.NewRequest("go.mock", r.Endpoint, map[string]interface{}{"foo": "bar"})
var rsp interface{}
err := c.Call(context.TODO(), req, &rsp)

View File

@@ -22,6 +22,9 @@ type Options struct {
Selector selector.Selector
Transport transport.Transport
// Router sets the router
Router Router
// Connection Pool
PoolSize int
PoolTTL time.Duration
@@ -99,7 +102,7 @@ func newOptions(options ...Option) Options {
}
if len(opts.ContentType) == 0 {
opts.ContentType = defaultContentType
opts.ContentType = DefaultContentType
}
if opts.Broker == nil {
@@ -306,3 +309,10 @@ func StreamingRequest() RequestOption {
o.Stream = true
}
}
// WithRouter sets the client router
func WithRouter(r Router) Option {
return func(o *Options) {
o.Router = r
}
}

View File

@@ -4,11 +4,14 @@ import (
"bytes"
"context"
"fmt"
"net"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"sync/atomic"
"github.com/google/uuid"
"github.com/micro/go-micro/broker"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/errors"
@@ -49,13 +52,18 @@ func (r *rpcClient) newCodec(contentType string) (codec.NewCodec, error) {
if c, ok := r.opts.Codecs[contentType]; ok {
return c, nil
}
if cf, ok := defaultCodecs[contentType]; ok {
if cf, ok := DefaultCodecs[contentType]; ok {
return cf, nil
}
return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
}
func (r *rpcClient) call(ctx context.Context, address string, req Request, resp interface{}, opts CallOptions) error {
func (r *rpcClient) call(ctx context.Context, node *registry.Node, req Request, resp interface{}, opts CallOptions) error {
address := node.Address
if node.Port > 0 {
address = fmt.Sprintf("%s:%d", address, node.Port)
}
msg := &transport.Message{
Header: make(map[string]string),
}
@@ -74,9 +82,16 @@ func (r *rpcClient) call(ctx context.Context, address string, req Request, resp
// set the accept header
msg.Header["Accept"] = req.ContentType()
cf, err := r.newCodec(req.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
// setup old protocol
cf := setupProtocol(msg, node)
// no codec specified
if cf == nil {
var err error
cf, err = r.newCodec(req.ContentType())
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
}
var grr error
@@ -91,13 +106,20 @@ func (r *rpcClient) call(ctx context.Context, address string, req Request, resp
seq := atomic.LoadUint64(&r.seq)
atomic.AddUint64(&r.seq, 1)
codec := newRpcCodec(msg, c, cf)
rsp := &rpcResponse{
socket: c,
codec: codec,
}
stream := &rpcStream{
context: ctx,
request: req,
closed: make(chan bool),
codec: newRpcPlusCodec(msg, c, cf),
seq: seq,
context: ctx,
request: req,
response: rsp,
codec: codec,
closed: make(chan bool),
id: fmt.Sprintf("%v", seq),
}
defer stream.Close()
@@ -111,7 +133,7 @@ func (r *rpcClient) call(ctx context.Context, address string, req Request, resp
}()
// send request
if err := stream.Send(req.Request()); err != nil {
if err := stream.Send(req.Body()); err != nil {
ch <- err
return
}
@@ -136,7 +158,12 @@ func (r *rpcClient) call(ctx context.Context, address string, req Request, resp
}
}
func (r *rpcClient) stream(ctx context.Context, address string, req Request, opts CallOptions) (Stream, error) {
func (r *rpcClient) stream(ctx context.Context, node *registry.Node, req Request, opts CallOptions) (Stream, error) {
address := node.Address
if node.Port > 0 {
address = fmt.Sprintf("%s:%d", address, node.Port)
}
msg := &transport.Message{
Header: make(map[string]string),
}
@@ -155,9 +182,16 @@ func (r *rpcClient) stream(ctx context.Context, address string, req Request, opt
// set the accept header
msg.Header["Accept"] = req.ContentType()
cf, err := r.newCodec(req.ContentType())
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
// set old codecs
cf := setupProtocol(msg, node)
// no codec specified
if cf == nil {
var err error
cf, err = r.newCodec(req.ContentType())
if err != nil {
return nil, errors.InternalServerError("go.micro.client", err.Error())
}
}
dOpts := []transport.DialOption{
@@ -173,17 +207,30 @@ func (r *rpcClient) stream(ctx context.Context, address string, req Request, opt
return nil, errors.InternalServerError("go.micro.client", "connection error: %v", err)
}
codec := newRpcCodec(msg, c, cf)
rsp := &rpcResponse{
socket: c,
codec: codec,
}
// set request codec
if r, ok := req.(*rpcRequest); ok {
r.codec = codec
}
stream := &rpcStream{
context: ctx,
request: req,
closed: make(chan bool),
codec: newRpcPlusCodec(msg, c, cf),
context: ctx,
request: req,
response: rsp,
closed: make(chan bool),
codec: codec,
}
ch := make(chan error, 1)
go func() {
ch <- stream.Send(req.Request())
ch <- stream.Send(req.Body())
}()
var grr error
@@ -227,21 +274,43 @@ func (r *rpcClient) Options() Options {
}
func (r *rpcClient) next(request Request, opts CallOptions) (selector.Next, error) {
service := request.Service()
// get proxy
if prx := os.Getenv("MICRO_PROXY"); len(prx) > 0 {
service = prx
}
// get proxy address
if prx := os.Getenv("MICRO_PROXY_ADDRESS"); len(prx) > 0 {
opts.Address = prx
}
// return remote address
if len(opts.Address) > 0 {
address := opts.Address
port := 0
host, sport, err := net.SplitHostPort(opts.Address)
if err == nil {
address = host
port, _ = strconv.Atoi(sport)
}
return func() (*registry.Node, error) {
return &registry.Node{
Address: opts.Address,
Address: address,
Port: port,
}, nil
}, nil
}
// get next nodes from the selector
next, err := r.opts.Selector.Select(request.Service(), opts.SelectOptions...)
next, err := r.opts.Selector.Select(service, opts.SelectOptions...)
if err != nil && err == selector.ErrNotFound {
return nil, errors.NotFound("go.micro.client", "service %s: %v", request.Service(), err.Error())
return nil, errors.NotFound("go.micro.client", "service %s: %v", service, err.Error())
} else if err != nil {
return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %v", request.Service(), err.Error())
return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %v", service, err.Error())
}
return next, nil
@@ -307,14 +376,8 @@ func (r *rpcClient) Call(ctx context.Context, request Request, response interfac
return errors.InternalServerError("go.micro.client", "error getting next %s node: %v", request.Service(), err.Error())
}
// set the address
address := node.Address
if node.Port > 0 {
address = fmt.Sprintf("%s:%d", address, node.Port)
}
// make the call
err = rcall(ctx, address, request, response, callOpts)
err = rcall(ctx, node, request, response, callOpts)
r.opts.Selector.Mark(request.Service(), node, err)
return err
}
@@ -390,12 +453,7 @@ func (r *rpcClient) Stream(ctx context.Context, request Request, opts ...CallOpt
return nil, errors.InternalServerError("go.micro.client", "error getting next %s node: %v", request.Service(), err.Error())
}
address := node.Address
if node.Port > 0 {
address = fmt.Sprintf("%s:%d", address, node.Port)
}
stream, err := r.stream(ctx, address, request, callOpts)
stream, err := r.stream(ctx, node, request, callOpts)
r.opts.Selector.Mark(request.Service(), node, err)
return stream, err
}
@@ -444,7 +502,11 @@ func (r *rpcClient) Publish(ctx context.Context, msg Message, opts ...PublishOpt
if !ok {
md = make(map[string]string)
}
id := uuid.New().String()
md["Content-Type"] = msg.ContentType()
md["Micro-Topic"] = msg.Topic()
md["Micro-Id"] = id
// encode message body
cf, err := r.newCodec(msg.ContentType())
@@ -452,7 +514,14 @@ func (r *rpcClient) Publish(ctx context.Context, msg Message, opts ...PublishOpt
return errors.InternalServerError("go.micro.client", err.Error())
}
b := &buffer{bytes.NewBuffer(nil)}
if err := cf(b).Write(&codec.Message{Type: codec.Publication}, msg.Payload()); err != nil {
if err := cf(b).Write(&codec.Message{
Target: msg.Topic(),
Type: codec.Publication,
Header: map[string]string{
"Micro-Id": id,
"Micro-Topic": msg.Topic(),
},
}, msg.Payload()); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
r.once.Do(func() {

View File

@@ -7,30 +7,41 @@ import (
"github.com/micro/go-micro/errors"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/mock"
"github.com/micro/go-micro/registry/memory"
"github.com/micro/go-micro/selector"
)
func newTestRegistry() registry.Registry {
r := memory.NewRegistry()
r.(*memory.Registry).Setup()
return r
}
func TestCallAddress(t *testing.T) {
var called bool
service := "test.service"
method := "Test.Method"
address := "10.1.10.1:8080"
endpoint := "Test.Endpoint"
address := "10.1.10.1"
port := 8080
wrap := func(cf CallFunc) CallFunc {
return func(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
return func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error {
called = true
if req.Service() != service {
return fmt.Errorf("expected service: %s got %s", service, req.Service())
}
if req.Method() != method {
return fmt.Errorf("expected service: %s got %s", method, req.Method())
if req.Endpoint() != endpoint {
return fmt.Errorf("expected service: %s got %s", endpoint, req.Endpoint())
}
if addr != address {
return fmt.Errorf("expected address: %s got %s", address, addr)
if node.Address != address {
return fmt.Errorf("expected address: %s got %s", address, node.Address)
}
if node.Port != port {
return fmt.Errorf("expected address: %d got %d", port, node.Port)
}
// don't do the call
@@ -38,17 +49,17 @@ func TestCallAddress(t *testing.T) {
}
}
r := mock.NewRegistry()
r := newTestRegistry()
c := NewClient(
Registry(r),
WrapCall(wrap),
)
c.Options().Selector.Init(selector.Registry(r))
req := c.NewRequest(service, method, nil)
req := c.NewRequest(service, endpoint, nil)
// test calling remote address
if err := c.Call(context.Background(), req, nil, WithAddress(address)); err != nil {
if err := c.Call(context.Background(), req, nil, WithAddress(fmt.Sprintf("%s:%d", address, port))); err != nil {
t.Fatal("call with address error", err)
}
@@ -60,13 +71,13 @@ func TestCallAddress(t *testing.T) {
func TestCallRetry(t *testing.T) {
service := "test.service"
method := "Test.Method"
address := "10.1.10.1:8080"
endpoint := "Test.Endpoint"
address := "10.1.10.1"
var called int
wrap := func(cf CallFunc) CallFunc {
return func(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
return func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error {
called++
if called == 1 {
return errors.InternalServerError("test.error", "retry request")
@@ -77,14 +88,14 @@ func TestCallRetry(t *testing.T) {
}
}
r := mock.NewRegistry()
r := newTestRegistry()
c := NewClient(
Registry(r),
WrapCall(wrap),
)
c.Options().Selector.Init(selector.Registry(r))
req := c.NewRequest(service, method, nil)
req := c.NewRequest(service, endpoint, nil)
// test calling remote address
if err := c.Call(context.Background(), req, nil, WithAddress(address)); err != nil {
@@ -101,25 +112,24 @@ func TestCallWrapper(t *testing.T) {
var called bool
id := "test.1"
service := "test.service"
method := "Test.Method"
host := "10.1.10.1"
endpoint := "Test.Endpoint"
address := "10.1.10.1"
port := 8080
address := "10.1.10.1:8080"
wrap := func(cf CallFunc) CallFunc {
return func(ctx context.Context, addr string, req Request, rsp interface{}, opts CallOptions) error {
return func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error {
called = true
if req.Service() != service {
return fmt.Errorf("expected service: %s got %s", service, req.Service())
}
if req.Method() != method {
return fmt.Errorf("expected service: %s got %s", method, req.Method())
if req.Endpoint() != endpoint {
return fmt.Errorf("expected service: %s got %s", endpoint, req.Endpoint())
}
if addr != address {
return fmt.Errorf("expected address: %s got %s", address, addr)
if node.Address != address {
return fmt.Errorf("expected address: %s got %s", address, node.Address)
}
// don't do the call
@@ -127,7 +137,7 @@ func TestCallWrapper(t *testing.T) {
}
}
r := mock.NewRegistry()
r := newTestRegistry()
c := NewClient(
Registry(r),
WrapCall(wrap),
@@ -140,13 +150,13 @@ func TestCallWrapper(t *testing.T) {
Nodes: []*registry.Node{
&registry.Node{
Id: id,
Address: host,
Address: address,
Port: port,
},
},
})
req := c.NewRequest(service, method, nil)
req := c.NewRequest(service, endpoint, nil)
if err := c.Call(context.Background(), req, nil); err != nil {
t.Fatal("call wrapper error", err)
}

View File

@@ -5,9 +5,14 @@ import (
errs "errors"
"github.com/micro/go-micro/codec"
raw "github.com/micro/go-micro/codec/bytes"
"github.com/micro/go-micro/codec/grpc"
"github.com/micro/go-micro/codec/json"
"github.com/micro/go-micro/codec/jsonrpc"
"github.com/micro/go-micro/codec/proto"
"github.com/micro/go-micro/codec/protorpc"
"github.com/micro/go-micro/errors"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/transport"
)
@@ -28,7 +33,7 @@ var (
errShutdown = errs.New("connection is shut down")
)
type rpcPlusCodec struct {
type rpcCodec struct {
client transport.Client
codec codec.Codec
@@ -41,31 +46,21 @@ type readWriteCloser struct {
rbuf *bytes.Buffer
}
type clientCodec interface {
WriteRequest(*request, interface{}) error
ReadResponseHeader(*response) error
ReadResponseBody(interface{}) error
Close() error
}
type request struct {
Service string
ServiceMethod string // format: "Service.Method"
Seq uint64 // sequence number chosen by client
next *request // for free list in Server
}
type response struct {
ServiceMethod string // echoes that of the Request
Seq uint64 // echoes that of the request
Error string // error, if any.
next *response // for free list in Server
}
var (
defaultContentType = "application/octet-stream"
DefaultContentType = "application/protobuf"
DefaultCodecs = map[string]codec.NewCodec{
"application/grpc": grpc.NewCodec,
"application/grpc+json": grpc.NewCodec,
"application/grpc+proto": grpc.NewCodec,
"application/protobuf": proto.NewCodec,
"application/json": json.NewCodec,
"application/json-rpc": jsonrpc.NewCodec,
"application/proto-rpc": protorpc.NewCodec,
"application/octet-stream": raw.NewCodec,
}
// TODO: remove legacy codec list
defaultCodecs = map[string]codec.NewCodec{
"application/json": jsonrpc.NewCodec,
"application/json-rpc": jsonrpc.NewCodec,
@@ -89,12 +84,77 @@ func (rwc *readWriteCloser) Close() error {
return nil
}
func newRpcPlusCodec(req *transport.Message, client transport.Client, c codec.NewCodec) *rpcPlusCodec {
func getHeaders(m *codec.Message) {
get := func(hdr string) string {
if hd := m.Header[hdr]; len(hd) > 0 {
return hd
}
// old
return m.Header["X-"+hdr]
}
// check error in header
if len(m.Error) == 0 {
m.Error = get("Micro-Error")
}
// check endpoint in header
if len(m.Endpoint) == 0 {
m.Endpoint = get("Micro-Endpoint")
}
// check method in header
if len(m.Method) == 0 {
m.Method = get("Micro-Method")
}
if len(m.Id) == 0 {
m.Id = get("Micro-Id")
}
}
func setHeaders(m *codec.Message) {
set := func(hdr, v string) {
if len(v) == 0 {
return
}
m.Header[hdr] = v
m.Header["X-"+hdr] = v
}
set("Micro-Id", m.Id)
set("Micro-Service", m.Target)
set("Micro-Method", m.Method)
set("Micro-Endpoint", m.Endpoint)
}
// setupProtocol sets up the old protocol
func setupProtocol(msg *transport.Message, node *registry.Node) codec.NewCodec {
protocol := node.Metadata["protocol"]
// got protocol
if len(protocol) > 0 {
return nil
}
// no protocol use old codecs
switch msg.Header["Content-Type"] {
case "application/json":
msg.Header["Content-Type"] = "application/json-rpc"
case "application/protobuf":
msg.Header["Content-Type"] = "application/proto-rpc"
}
// now return codec
return defaultCodecs[msg.Header["Content-Type"]]
}
func newRpcCodec(req *transport.Message, client transport.Client, c codec.NewCodec) codec.Codec {
rwc := &readWriteCloser{
wbuf: bytes.NewBuffer(nil),
rbuf: bytes.NewBuffer(nil),
}
r := &rpcPlusCodec{
r := &rpcCodec{
buf: rwc,
client: client,
codec: c(rwc),
@@ -103,58 +163,90 @@ func newRpcPlusCodec(req *transport.Message, client transport.Client, c codec.Ne
return r
}
func (c *rpcPlusCodec) WriteRequest(req *request, body interface{}) error {
func (c *rpcCodec) Write(m *codec.Message, body interface{}) error {
c.buf.wbuf.Reset()
m := &codec.Message{
Id: req.Seq,
Target: req.Service,
Method: req.ServiceMethod,
Type: codec.Request,
Header: map[string]string{
"X-Micro-Target": req.Service,
"X-Micro-Method": req.ServiceMethod,
},
// create header
if m.Header == nil {
m.Header = map[string]string{}
}
if err := c.codec.Write(m, body); err != nil {
return errors.InternalServerError("go.micro.client.codec", err.Error())
// copy original header
for k, v := range c.req.Header {
m.Header[k] = v
}
c.req.Body = c.buf.wbuf.Bytes()
for k, v := range m.Header {
c.req.Header[k] = v
// set the mucp headers
setHeaders(m)
// if body is bytes Frame don't encode
if body != nil {
b, ok := body.(*raw.Frame)
if ok {
// set body
m.Body = b.Data
body = nil
}
}
if err := c.client.Send(c.req); err != nil {
if len(m.Body) == 0 {
// write to codec
if err := c.codec.Write(m, body); err != nil {
return errors.InternalServerError("go.micro.client.codec", err.Error())
}
// set body
m.Body = c.buf.wbuf.Bytes()
}
// create new transport message
msg := transport.Message{
Header: m.Header,
Body: m.Body,
}
// send the request
if err := c.client.Send(&msg); err != nil {
return errors.InternalServerError("go.micro.client.transport", err.Error())
}
return nil
}
func (c *rpcPlusCodec) ReadResponseHeader(r *response) error {
var m transport.Message
if err := c.client.Recv(&m); err != nil {
func (c *rpcCodec) ReadHeader(m *codec.Message, r codec.MessageType) error {
var tm transport.Message
// read message from transport
if err := c.client.Recv(&tm); err != nil {
return errors.InternalServerError("go.micro.client.transport", err.Error())
}
c.buf.rbuf.Reset()
c.buf.rbuf.Write(m.Body)
var me codec.Message
err := c.codec.ReadHeader(&me, codec.Response)
r.ServiceMethod = me.Method
r.Seq = me.Id
r.Error = me.Error
c.buf.rbuf.Write(tm.Body)
// set headers from transport
m.Header = tm.Header
// read header
err := c.codec.ReadHeader(m, r)
// get headers
getHeaders(m)
// return header error
if err != nil {
return errors.InternalServerError("go.micro.client.codec", err.Error())
}
return nil
}
func (c *rpcPlusCodec) ReadResponseBody(b interface{}) error {
func (c *rpcCodec) ReadBody(b interface{}) error {
// read body
if err := c.codec.ReadBody(b); err != nil {
return errors.InternalServerError("go.micro.client.codec", err.Error())
}
return nil
}
func (c *rpcPlusCodec) Close() error {
func (c *rpcCodec) Close() error {
c.buf.Close()
c.codec.Close()
if err := c.client.Close(); err != nil {
@@ -162,3 +254,7 @@ func (c *rpcPlusCodec) Close() error {
}
return nil
}
func (c *rpcCodec) String() string {
return "rpc"
}

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/micro/go-micro/transport"
"github.com/micro/go-micro/transport/mock"
"github.com/micro/go-micro/transport/memory"
)
func testPool(t *testing.T, size int, ttl time.Duration) {
@@ -13,7 +13,7 @@ func testPool(t *testing.T, size int, ttl time.Duration) {
p := newPool(size, ttl)
// mock transport
tr := mock.NewTransport()
tr := memory.NewTransport()
// listen
l, err := tr.Listen(":0")

View File

@@ -1,14 +1,20 @@
package client
import (
"github.com/micro/go-micro/codec"
)
type rpcRequest struct {
service string
method string
endpoint string
contentType string
request interface{}
codec codec.Codec
body interface{}
opts RequestOptions
}
func newRequest(service, method string, request interface{}, contentType string, reqOpts ...RequestOption) Request {
func newRequest(service, endpoint string, request interface{}, contentType string, reqOpts ...RequestOption) Request {
var opts RequestOptions
for _, o := range reqOpts {
@@ -22,8 +28,9 @@ func newRequest(service, method string, request interface{}, contentType string,
return &rpcRequest{
service: service,
method: method,
request: request,
method: endpoint,
endpoint: endpoint,
body: request,
contentType: contentType,
opts: opts,
}
@@ -41,8 +48,16 @@ func (r *rpcRequest) Method() string {
return r.method
}
func (r *rpcRequest) Request() interface{} {
return r.request
func (r *rpcRequest) Endpoint() string {
return r.endpoint
}
func (r *rpcRequest) Body() interface{} {
return r.body
}
func (r *rpcRequest) Codec() codec.Writer {
return r.codec
}
func (r *rpcRequest) Stream() bool {

View File

@@ -5,19 +5,19 @@ import (
)
func TestRequestOptions(t *testing.T) {
r := newRequest("service", "method", nil, "application/json")
r := newRequest("service", "endpoint", nil, "application/json")
if r.Service() != "service" {
t.Fatalf("expected 'service' got %s", r.Service())
}
if r.Method() != "method" {
t.Fatalf("expected 'method' got %s", r.Method())
if r.Endpoint() != "endpoint" {
t.Fatalf("expected 'endpoint' got %s", r.Endpoint())
}
if r.ContentType() != "application/json" {
t.Fatalf("expected 'method' got %s", r.ContentType())
t.Fatalf("expected 'endpoint' got %s", r.ContentType())
}
r2 := newRequest("service", "method", nil, "application/json", WithContentType("application/protobuf"))
r2 := newRequest("service", "endpoint", nil, "application/json", WithContentType("application/protobuf"))
if r2.ContentType() != "application/protobuf" {
t.Fatalf("expected 'method' got %s", r2.ContentType())
t.Fatalf("expected 'endpoint' got %s", r2.ContentType())
}
}

35
client/rpc_response.go Normal file
View File

@@ -0,0 +1,35 @@
package client
import (
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/transport"
)
type rpcResponse struct {
header map[string]string
body []byte
socket transport.Socket
codec codec.Codec
}
func (r *rpcResponse) Codec() codec.Reader {
return r.codec
}
func (r *rpcResponse) Header() map[string]string {
return r.header
}
func (r *rpcResponse) Read() ([]byte, error) {
var msg transport.Message
if err := r.socket.Recv(&msg); err != nil {
return nil, err
}
// set internals
r.header = msg.Header
r.body = msg.Body
return msg.Body, nil
}

View File

@@ -4,17 +4,20 @@ import (
"context"
"io"
"sync"
"github.com/micro/go-micro/codec"
)
// Implements the streamer interface
type rpcStream struct {
sync.RWMutex
seq uint64
closed chan bool
err error
request Request
codec clientCodec
context context.Context
id string
closed chan bool
err error
request Request
response Response
codec codec.Codec
context context.Context
}
func (r *rpcStream) isClosed() bool {
@@ -34,6 +37,10 @@ func (r *rpcStream) Request() Request {
return r.request
}
func (r *rpcStream) Response() Response {
return r.response
}
func (r *rpcStream) Send(msg interface{}) error {
r.Lock()
defer r.Unlock()
@@ -43,18 +50,19 @@ func (r *rpcStream) Send(msg interface{}) error {
return errShutdown
}
seq := r.seq
req := request{
Service: r.request.Service(),
Seq: seq,
ServiceMethod: r.request.Method(),
req := codec.Message{
Id: r.id,
Target: r.request.Service(),
Method: r.request.Method(),
Endpoint: r.request.Endpoint(),
Type: codec.Request,
}
if err := r.codec.WriteRequest(&req, msg); err != nil {
if err := r.codec.Write(&req, msg); err != nil {
r.err = err
return err
}
return nil
}
@@ -67,8 +75,9 @@ func (r *rpcStream) Recv(msg interface{}) error {
return errShutdown
}
var resp response
if err := r.codec.ReadResponseHeader(&resp); err != nil {
var resp codec.Message
if err := r.codec.ReadHeader(&resp, codec.Response); err != nil {
if err == io.EOF && !r.isClosed() {
r.err = io.ErrUnexpectedEOF
return io.ErrUnexpectedEOF
@@ -87,11 +96,11 @@ func (r *rpcStream) Recv(msg interface{}) error {
} else {
r.err = io.EOF
}
if err := r.codec.ReadResponseBody(nil); err != nil {
if err := r.codec.ReadBody(nil); err != nil {
r.err = err
}
default:
if err := r.codec.ReadResponseBody(msg); err != nil {
if err := r.codec.ReadBody(msg); err != nil {
r.err = err
}
}

View File

@@ -2,10 +2,12 @@ package client
import (
"context"
"github.com/micro/go-micro/registry"
)
// CallFunc represents the individual call func
type CallFunc func(ctx context.Context, address string, req Request, rsp interface{}, opts CallOptions) error
type CallFunc func(ctx context.Context, node *registry.Node, req Request, rsp interface{}, opts CallOptions) error
// CallWrapper is a low level wrapper for the CallFunc
type CallWrapper func(CallFunc) CallFunc

View File

@@ -17,19 +17,24 @@ import (
// brokers
"github.com/micro/go-micro/broker"
"github.com/micro/go-micro/broker/http"
"github.com/micro/go-micro/broker/memory"
// registries
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/registry/consul"
"github.com/micro/go-micro/registry/gossip"
"github.com/micro/go-micro/registry/mdns"
rmem "github.com/micro/go-micro/registry/memory"
// selectors
"github.com/micro/go-micro/selector"
"github.com/micro/go-micro/selector/dns"
"github.com/micro/go-micro/selector/static"
// transports
"github.com/micro/go-micro/transport"
thttp "github.com/micro/go-micro/transport/http"
tmem "github.com/micro/go-micro/transport/memory"
)
type Cmd interface {
@@ -162,7 +167,8 @@ var (
}
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
"http": http.NewBroker,
"http": http.NewBroker,
"memory": memory.NewBroker,
}
DefaultClients = map[string]func(...client.Option) client.Client{
@@ -173,11 +179,14 @@ var (
"consul": consul.NewRegistry,
"gossip": gossip.NewRegistry,
"mdns": mdns.NewRegistry,
"memory": rmem.NewRegistry,
}
DefaultSelectors = map[string]func(...selector.Option) selector.Selector{
"default": selector.NewSelector,
"dns": dns.NewSelector,
"cache": selector.NewSelector,
"static": static.NewSelector,
}
DefaultServers = map[string]func(...server.Option) server.Server{
@@ -185,15 +194,16 @@ var (
}
DefaultTransports = map[string]func(...transport.Option) transport.Transport{
"http": thttp.NewTransport,
"memory": tmem.NewTransport,
"http": thttp.NewTransport,
}
// used for default selection as the fall back
defaultClient = "rpc"
defaultServer = "rpc"
defaultBroker = "http"
defaultRegistry = "consul"
defaultSelector = "cache"
defaultRegistry = "mdns"
defaultSelector = "registry"
defaultTransport = "http"
)
@@ -394,6 +404,10 @@ func (c *cmd) Before(ctx *cli.Context) error {
serverOpts = append(serverOpts, server.RegisterTTL(ttl*time.Second))
}
if val := time.Duration(ctx.GlobalInt("register_interval")); val > 0 {
serverOpts = append(serverOpts, server.RegisterInterval(val*time.Second))
}
// client opts
if r := ctx.Int("client_retries"); r >= 0 {
clientOpts = append(clientOpts, client.Retries(r))

75
codec/bytes/bytes.go Normal file
View File

@@ -0,0 +1,75 @@
// Package bytes provides a bytes codec which does not encode or decode anything
package bytes
import (
"fmt"
"io"
"io/ioutil"
"github.com/micro/go-micro/codec"
)
type Codec struct {
Conn io.ReadWriteCloser
}
// Frame gives us the ability to define raw data to send over the pipes
type Frame struct {
Data []byte
}
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
return nil
}
func (c *Codec) ReadBody(b interface{}) error {
// read bytes
buf, err := ioutil.ReadAll(c.Conn)
if err != nil {
return err
}
switch b.(type) {
case *[]byte:
v := b.(*[]byte)
*v = buf
case *Frame:
v := b.(*Frame)
v.Data = buf
default:
return fmt.Errorf("failed to read body: %v is not type of *[]byte", b)
}
return nil
}
func (c *Codec) Write(m *codec.Message, b interface{}) error {
var v []byte
switch b.(type) {
case *Frame:
v = b.(*Frame).Data
case *[]byte:
ve := b.(*[]byte)
v = *ve
case []byte:
v = b.([]byte)
default:
return fmt.Errorf("failed to write: %v is not type of *[]byte or []byte", b)
}
_, err := c.Conn.Write(v)
return err
}
func (c *Codec) Close() error {
return c.Conn.Close()
}
func (c *Codec) String() string {
return "bytes"
}
func NewCodec(c io.ReadWriteCloser) codec.Codec {
return &Codec{
Conn: c,
}
}

41
codec/bytes/marshaler.go Normal file
View File

@@ -0,0 +1,41 @@
package bytes
import (
"errors"
)
type Marshaler struct{}
type Message struct {
Header map[string]string
Body []byte
}
func (n Marshaler) Marshal(v interface{}) ([]byte, error) {
switch v.(type) {
case *[]byte:
ve := v.(*[]byte)
return *ve, nil
case []byte:
return v.([]byte), nil
case *Message:
return v.(*Message).Body, nil
}
return nil, errors.New("invalid message")
}
func (n Marshaler) Unmarshal(d []byte, v interface{}) error {
switch v.(type) {
case *[]byte:
ve := v.(*[]byte)
*ve = d
case *Message:
ve := v.(*Message)
ve.Body = d
}
return errors.New("invalid message")
}
func (n Marshaler) String() string {
return "bytes"
}

View File

@@ -23,10 +23,26 @@ type NewCodec func(io.ReadWriteCloser) Codec
// connection. ReadBody may be called with a nil argument to force the
// body to be read and discarded.
type Codec interface {
Reader
Writer
Close() error
String() string
}
type Reader interface {
ReadHeader(*Message, MessageType) error
ReadBody(interface{}) error
}
type Writer interface {
Write(*Message, interface{}) error
Close() error
}
// Marshaler is a simple encoding interface used for the broker/transport
// where headers are not supported by the underlying implementation.
type Marshaler interface {
Marshal(interface{}) ([]byte, error)
Unmarshal([]byte, interface{}) error
String() string
}
@@ -34,10 +50,14 @@ type Codec interface {
// the communication, likely followed by the body.
// In the case of an error, body may be nil.
type Message struct {
Id uint64
Type MessageType
Target string
Method string
Error string
Id string
Type MessageType
Target string
Method string
Endpoint string
Error string
// The values read from the socket
Header map[string]string
Body []byte
}

132
codec/grpc/grpc.go Normal file
View File

@@ -0,0 +1,132 @@
// Package grpc provides a grpc codec
package grpc
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"github.com/golang/protobuf/proto"
"github.com/micro/go-micro/codec"
)
type Codec struct {
Conn io.ReadWriteCloser
ContentType string
}
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
if ct := m.Header["Content-Type"]; len(ct) > 0 {
c.ContentType = ct
}
if ct := m.Header["content-type"]; len(ct) > 0 {
c.ContentType = ct
}
// service method
path := m.Header[":path"]
if len(path) == 0 || path[0] != '/' {
m.Target = m.Header["Micro-Service"]
m.Endpoint = m.Header["Micro-Endpoint"]
} else {
// [ , a.package.Foo, Bar]
parts := strings.Split(path, "/")
if len(parts) != 3 {
return errors.New("Unknown request path")
}
service := strings.Split(parts[1], ".")
m.Endpoint = strings.Join([]string{service[len(service)-1], parts[2]}, ".")
m.Target = strings.Join(service[:len(service)-1], ".")
}
return nil
}
func (c *Codec) ReadBody(b interface{}) error {
// no body
if b == nil {
return nil
}
_, buf, err := decode(c.Conn)
if err != nil {
return err
}
switch c.ContentType {
case "application/grpc+json":
return json.Unmarshal(buf, b)
case "application/grpc+proto", "application/grpc":
return proto.Unmarshal(buf, b.(proto.Message))
}
return errors.New("Unsupported Content-Type")
}
func (c *Codec) Write(m *codec.Message, b interface{}) error {
var buf []byte
var err error
if ct := m.Header["Content-Type"]; len(ct) > 0 {
c.ContentType = ct
}
if ct := m.Header["content-type"]; len(ct) > 0 {
c.ContentType = ct
}
switch m.Type {
case codec.Request:
parts := strings.Split(m.Endpoint, ".")
m.Header[":method"] = "POST"
m.Header[":path"] = fmt.Sprintf("/%s.%s/%s", m.Target, parts[0], parts[1])
m.Header[":proto"] = "HTTP/2.0"
m.Header["te"] = "trailers"
m.Header["user-agent"] = "grpc-go/1.0.0"
m.Header[":authority"] = m.Target
m.Header["content-type"] = c.ContentType
case codec.Response:
m.Header["Trailer"] = "grpc-status, grpc-message"
m.Header["grpc-status"] = "0"
m.Header["grpc-message"] = ""
}
// marshal content
switch c.ContentType {
case "application/grpc+json":
buf, err = json.Marshal(b)
case "application/grpc+proto", "application/grpc":
pb, ok := b.(proto.Message)
if ok {
buf, err = proto.Marshal(pb)
}
default:
err = errors.New("Unsupported Content-Type")
}
// check error
if err != nil {
m.Header["grpc-status"] = "8"
m.Header["grpc-message"] = err.Error()
return err
}
return encode(0, buf, c.Conn)
}
func (c *Codec) Close() error {
return c.Conn.Close()
}
func (c *Codec) String() string {
return "grpc"
}
func NewCodec(c io.ReadWriteCloser) codec.Codec {
return &Codec{
Conn: c,
ContentType: "application/grpc",
}
}

70
codec/grpc/util.go Normal file
View File

@@ -0,0 +1,70 @@
package grpc
import (
"encoding/binary"
"fmt"
"io"
)
var (
maxMessageSize = 1024 * 1024 * 4
maxInt = int(^uint(0) >> 1)
)
func decode(r io.Reader) (uint8, []byte, error) {
header := make([]byte, 5)
// read the header
if _, err := r.Read(header[:]); err != nil {
return uint8(0), nil, err
}
// get encoding format e.g compressed
cf := uint8(header[0])
// get message length
length := binary.BigEndian.Uint32(header[1:])
// no encoding format
if length == 0 {
return cf, nil, nil
}
//
if int64(length) > int64(maxInt) {
return cf, nil, fmt.Errorf("grpc: received message larger than max length allowed on current machine (%d vs. %d)", length, maxInt)
}
if int(length) > maxMessageSize {
return cf, nil, fmt.Errorf("grpc: received message larger than max (%d vs. %d)", length, maxMessageSize)
}
msg := make([]byte, int(length))
if _, err := r.Read(msg); err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return cf, nil, err
}
return cf, msg, nil
}
func encode(cf uint8, buf []byte, w io.Writer) error {
header := make([]byte, 5)
// set compression
header[0] = byte(cf)
// write length as header
binary.BigEndian.PutUint32(header[1:], uint32(len(buf)))
// read the header
if _, err := w.Write(header[:]); err != nil {
return err
}
// write the buffer
_, err := w.Write(buf)
return err
}

49
codec/json/json.go Normal file
View File

@@ -0,0 +1,49 @@
// Package json provides a json codec
package json
import (
"encoding/json"
"io"
"github.com/micro/go-micro/codec"
)
type Codec struct {
Conn io.ReadWriteCloser
Encoder *json.Encoder
Decoder *json.Decoder
}
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
return nil
}
func (c *Codec) ReadBody(b interface{}) error {
if b == nil {
return nil
}
return c.Decoder.Decode(b)
}
func (c *Codec) Write(m *codec.Message, b interface{}) error {
if b == nil {
return nil
}
return c.Encoder.Encode(b)
}
func (c *Codec) Close() error {
return c.Conn.Close()
}
func (c *Codec) String() string {
return "json"
}
func NewCodec(c io.ReadWriteCloser) codec.Codec {
return &Codec{
Conn: c,
Decoder: json.NewDecoder(c),
Encoder: json.NewEncoder(c),
}
}

19
codec/json/marshaler.go Normal file
View File

@@ -0,0 +1,19 @@
package json
import (
"encoding/json"
)
type Marshaler struct{}
func (j Marshaler) Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (j Marshaler) Unmarshal(d []byte, v interface{}) error {
return json.Unmarshal(d, v)
}
func (j Marshaler) String() string {
return "json"
}

View File

@@ -19,17 +19,17 @@ type clientCodec struct {
resp clientResponse
sync.Mutex
pending map[uint64]string
pending map[interface{}]string
}
type clientRequest struct {
Method string `json:"method"`
Params [1]interface{} `json:"params"`
ID uint64 `json:"id"`
ID interface{} `json:"id"`
}
type clientResponse struct {
ID uint64 `json:"id"`
ID interface{} `json:"id"`
Result *json.RawMessage `json:"result"`
Error interface{} `json:"error"`
}
@@ -39,7 +39,7 @@ func newClientCodec(conn io.ReadWriteCloser) *clientCodec {
dec: json.NewDecoder(conn),
enc: json.NewEncoder(conn),
c: conn,
pending: make(map[uint64]string),
pending: make(map[interface{}]string),
}
}
@@ -71,7 +71,7 @@ func (c *clientCodec) ReadHeader(m *codec.Message) error {
c.Unlock()
m.Error = ""
m.Id = c.resp.ID
m.Id = fmt.Sprintf("%v", c.resp.ID)
if c.resp.Error != nil {
x, ok := c.resp.Error.(string)
if !ok {

View File

@@ -31,7 +31,7 @@ func (j *jsonCodec) Write(m *codec.Message, b interface{}) error {
switch m.Type {
case codec.Request:
return j.c.Write(m, b)
case codec.Response:
case codec.Response, codec.Error:
return j.s.Write(m, b)
case codec.Publication:
data, err := json.Marshal(b)

View File

@@ -2,9 +2,8 @@ package jsonrpc
import (
"encoding/json"
"errors"
"fmt"
"io"
"sync"
"github.com/micro/go-micro/codec"
)
@@ -17,30 +16,25 @@ type serverCodec struct {
// temporary work space
req serverRequest
resp serverResponse
sync.Mutex
seq uint64
pending map[uint64]*json.RawMessage
}
type serverRequest struct {
Method string `json:"method"`
Params *json.RawMessage `json:"params"`
ID *json.RawMessage `json:"id"`
ID interface{} `json:"id"`
}
type serverResponse struct {
ID *json.RawMessage `json:"id"`
Result interface{} `json:"result"`
Error interface{} `json:"error"`
ID interface{} `json:"id"`
Result interface{} `json:"result"`
Error interface{} `json:"error"`
}
func newServerCodec(conn io.ReadWriteCloser) *serverCodec {
return &serverCodec{
dec: json.NewDecoder(conn),
enc: json.NewEncoder(conn),
c: conn,
pending: make(map[uint64]*json.RawMessage),
dec: json.NewDecoder(conn),
enc: json.NewEncoder(conn),
c: conn,
}
}
@@ -50,7 +44,7 @@ func (r *serverRequest) reset() {
*r.Params = (*r.Params)[0:0]
}
if r.ID != nil {
*r.ID = (*r.ID)[0:0]
r.ID = nil
}
}
@@ -60,14 +54,8 @@ func (c *serverCodec) ReadHeader(m *codec.Message) error {
return err
}
m.Method = c.req.Method
c.Lock()
c.seq++
c.pending[c.seq] = c.req.ID
m.Id = fmt.Sprintf("%v", c.req.ID)
c.req.ID = nil
m.Id = c.seq
c.Unlock()
return nil
}
@@ -84,19 +72,7 @@ var null = json.RawMessage([]byte("null"))
func (c *serverCodec) Write(m *codec.Message, x interface{}) error {
var resp serverResponse
c.Lock()
b, ok := c.pending[m.Id]
if !ok {
c.Unlock()
return errors.New("invalid sequence number in response")
}
c.Unlock()
if b == nil {
// Invalid request so no id. Use JSON null.
b = &null
}
resp.ID = b
resp.ID = m.Id
resp.Result = x
if m.Error == "" {
resp.Error = nil

19
codec/proto/marshaler.go Normal file
View File

@@ -0,0 +1,19 @@
package proto
import (
"github.com/golang/protobuf/proto"
)
type Marshaler struct{}
func (Marshaler) Marshal(v interface{}) ([]byte, error) {
return proto.Marshal(v.(proto.Message))
}
func (Marshaler) Unmarshal(data []byte, v interface{}) error {
return proto.Unmarshal(data, v.(proto.Message))
}
func (Marshaler) Name() string {
return "proto"
}

56
codec/proto/proto.go Normal file
View File

@@ -0,0 +1,56 @@
// Package proto provides a proto codec
package proto
import (
"io"
"io/ioutil"
"github.com/golang/protobuf/proto"
"github.com/micro/go-micro/codec"
)
type Codec struct {
Conn io.ReadWriteCloser
}
func (c *Codec) ReadHeader(m *codec.Message, t codec.MessageType) error {
return nil
}
func (c *Codec) ReadBody(b interface{}) error {
if b == nil {
return nil
}
buf, err := ioutil.ReadAll(c.Conn)
if err != nil {
return err
}
return proto.Unmarshal(buf, b.(proto.Message))
}
func (c *Codec) Write(m *codec.Message, b interface{}) error {
p, ok := b.(proto.Message)
if !ok {
return nil
}
buf, err := proto.Marshal(p)
if err != nil {
return err
}
_, err = c.Conn.Write(buf)
return err
}
func (c *Codec) Close() error {
return c.Conn.Close()
}
func (c *Codec) String() string {
return "proto"
}
func NewCodec(c io.ReadWriteCloser) codec.Codec {
return &Codec{
Conn: c,
}
}

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"fmt"
"io"
"strconv"
"sync"
"github.com/golang/protobuf/proto"
@@ -31,13 +32,22 @@ func (c *protoCodec) String() string {
return "proto-rpc"
}
func id(id string) *uint64 {
p, err := strconv.ParseInt(id, 10, 64)
if err != nil {
p = 0
}
i := uint64(p)
return &i
}
func (c *protoCodec) Write(m *codec.Message, b interface{}) error {
switch m.Type {
case codec.Request:
c.Lock()
defer c.Unlock()
// This is protobuf, of course we copy it.
pbr := &Request{ServiceMethod: &m.Method, Seq: &m.Id}
pbr := &Request{ServiceMethod: &m.Method, Seq: id(m.Id)}
data, err := proto.Marshal(pbr)
if err != nil {
return err
@@ -60,10 +70,10 @@ func (c *protoCodec) Write(m *codec.Message, b interface{}) error {
return err
}
}
case codec.Response:
case codec.Response, codec.Error:
c.Lock()
defer c.Unlock()
rtmp := &Response{ServiceMethod: &m.Method, Seq: &m.Id, Error: &m.Error}
rtmp := &Response{ServiceMethod: &m.Method, Seq: id(m.Id), Error: &m.Error}
data, err := proto.Marshal(rtmp)
if err != nil {
return err
@@ -117,7 +127,7 @@ func (c *protoCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
return err
}
m.Method = rtmp.GetServiceMethod()
m.Id = rtmp.GetSeq()
m.Id = fmt.Sprintf("%d", rtmp.GetSeq())
case codec.Response:
data, err := ReadNetString(c.rwc)
if err != nil {
@@ -129,7 +139,7 @@ func (c *protoCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
return err
}
m.Method = rtmp.GetServiceMethod()
m.Id = rtmp.GetSeq()
m.Id = fmt.Sprintf("%d", rtmp.GetSeq())
m.Error = rtmp.GetError()
case codec.Publication:
_, err := io.Copy(c.buf, c.rwc)

View File

@@ -5,7 +5,7 @@ import (
"sync"
"testing"
"github.com/micro/go-micro/registry/mock"
"github.com/micro/go-micro/registry/memory"
proto "github.com/micro/go-micro/server/debug/proto"
)
@@ -13,9 +13,12 @@ func TestFunction(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
r := memory.NewRegistry()
r.(*memory.Registry).Setup()
// create service
fn := NewFunction(
Registry(mock.NewRegistry()),
Registry(r),
Name("test.function"),
AfterStart(func() error {
wg.Done()

29
go.mod Normal file
View File

@@ -0,0 +1,29 @@
module github.com/micro/go-micro
require (
github.com/armon/go-radix v1.0.0 // indirect
github.com/go-log/log v0.1.0
github.com/golang/protobuf v1.2.0
github.com/google/uuid v1.1.0
github.com/hashicorp/consul v1.4.2
github.com/hashicorp/go-sockaddr v1.0.1 // indirect
github.com/hashicorp/memberlist v0.1.3
github.com/mattn/go-colorable v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/micro/cli v0.1.0
github.com/micro/go-log v0.1.0
github.com/micro/go-rcache v0.1.0
github.com/micro/h2c v1.0.0
github.com/micro/mdns v0.1.0
github.com/micro/util v0.1.0
github.com/miekg/dns v1.1.4 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure v1.0.0
github.com/pkg/errors v0.8.1
github.com/posener/complete v1.2.1 // indirect
github.com/stretchr/objx v0.1.1 // indirect
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 // indirect
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd
golang.org/x/sys v0.0.0-20190213121743-983097b1a8a3 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

121
go.sum Normal file
View File

@@ -0,0 +1,121 @@
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/go-log/log v0.1.0 h1:wudGTNsiGzrD5ZjgIkVZ517ugi2XRe9Q/xRCzwEO4/U=
github.com/go-log/log v0.1.0/go.mod h1:4mBwpdRMFLiuXZDCwU2lKQFsoSCo72j3HqBK9d81N2M=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/consul v1.4.2 h1:D9iJoJb8Ehe/Zmr+UEE3U3FjOLZ4LUxqFMl4O43BM1U=
github.com/hashicorp/consul v1.4.2/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.1 h1:eCkkJ5KOOktDvwbsE9KPyiBWaOfp1ZNy2gLHgL8PSBM=
github.com/hashicorp/go-sockaddr v1.0.1/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/micro/cli v0.0.0-20181223203424-1b0c9793c300/go.mod h1:x9x6qy+tXv17jzYWQup462+j3SIUgDa6vVTzU4IXy/w=
github.com/micro/cli v0.1.0 h1:5DT+QdbAPPQvB3gYTgwze7tFO1m+7DU1sz9XfQczbsc=
github.com/micro/cli v0.1.0/go.mod h1:jRT9gmfVKWSS6pkKcXQ8YhUyj6bzwxK8Fp5b0Y7qNnk=
github.com/micro/go-log v0.1.0 h1:szYSR+yyTsomZM2jyinJC5562DlqffSjHmTZFaeZ2vY=
github.com/micro/go-log v0.1.0/go.mod h1:qaFBF6d6Jk01Gz4cbMkCA2vVuLk3FSaLLjmEGrMCreA=
github.com/micro/go-micro v0.23.0/go.mod h1:3z3lfMkNU9Sr1L/CxL++8pVJmQapRo0N6kNjwYDtOVs=
github.com/micro/go-rcache v0.1.0 h1:YTIgANVHgBe1XOQ/yLICL+s2gbZCAdW+c2ckhekjkuc=
github.com/micro/go-rcache v0.1.0/go.mod h1:INzyZjXO5M+PmN2A33YxD4TaOY61xjFIM4CfSHv+At8=
github.com/micro/h2c v1.0.0/go.mod h1:54sOOQW/GRlHhH43vKwOhUb+kHaXhVxR0d3CJhn9alE=
github.com/micro/mdns v0.0.0-20181201230301-9c3770d4057a/go.mod h1:SQG6o/94RinohLuB5noHSevg2Iqg2wXLDUn4lj2LWWo=
github.com/micro/mdns v0.1.0 h1:fuLybUsfynbigJmCot/54i+gwe0hpc/vtCMvWt2WfDI=
github.com/micro/mdns v0.1.0/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc=
github.com/micro/util v0.1.0 h1:ghhF5KKRNlKMexzK+cWo6W6uRAZdKy1UKG/9O74NCYc=
github.com/micro/util v0.1.0/go.mod h1:MZgOs0nwxzv9k4xQo4fpF9IwZGF2O96F5/phP9X4/Sw=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPbK47CNHdWl3h/PJtii0=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190213121743-983097b1a8a3 h1:+KlxhGbYkFs8lMfwKn+2ojry1ID5eBSMXprS2u/wqCE=
golang.org/x/sys v0.0.0-20190213121743-983097b1a8a3/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -12,6 +12,14 @@ type metaKey struct{}
// from Transport headers.
type Metadata map[string]string
func Copy(md Metadata) Metadata {
cmd := make(Metadata)
for k, v := range md {
cmd[k] = v
}
return cmd
}
func FromContext(ctx context.Context) (Metadata, bool) {
md, ok := ctx.Value(metaKey{}).(Metadata)
return md, ok

View File

@@ -5,6 +5,21 @@ import (
"testing"
)
func TestMetadataCopy(t *testing.T) {
md := Metadata{
"foo": "bar",
"bar": "baz",
}
cp := Copy(md)
for k, v := range md {
if cv := cp[k]; cv != v {
t.Fatalf("Got %s:%s for %s:%s", k, cv, k, v)
}
}
}
func TestMetadataContext(t *testing.T) {
md := Metadata{
"foo": "bar",

View File

@@ -1,4 +1,4 @@
// Package micro is a pluggable RPC framework for microservices
// Package micro is a pluggable framework for microservices
package micro
import (
@@ -42,7 +42,7 @@ type Publisher interface {
type Option func(*Options)
var (
HeaderPrefix = "X-Micro-"
HeaderPrefix = "Micro-"
)
// NewService creates and returns a new Service based on the packages within.

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -22,9 +22,6 @@ type Options struct {
Registry registry.Registry
Transport transport.Transport
// Register loop interval
RegisterInterval time.Duration
// Before and After funcs
BeforeStart []func() error
BeforeStop []func() error
@@ -125,6 +122,13 @@ func Transport(t transport.Transport) Option {
// Convenience options
// Address sets the address of the server
func Address(addr string) Option {
return func(o *Options) {
o.Server.Init(server.Address(addr))
}
}
// Name of the service
func Name(n string) Option {
return func(o *Options) {
@@ -168,7 +172,7 @@ func RegisterTTL(t time.Duration) Option {
// RegisterInterval specifies the interval on which to re-register
func RegisterInterval(t time.Duration) Option {
return func(o *Options) {
o.RegisterInterval = t
o.Server.Init(server.RegisterInterval(t))
}
}

View File

@@ -1,11 +1,387 @@
// Package consul provides a consul based registry and is the default discovery system
package consul
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"runtime"
"sync"
"time"
consul "github.com/hashicorp/consul/api"
"github.com/micro/go-micro/registry"
hash "github.com/mitchellh/hashstructure"
)
// NewRegistry returns a new consul registry
func NewRegistry(opts ...registry.Option) registry.Registry {
return registry.NewRegistry(opts...)
type consulRegistry struct {
Address string
Client *consul.Client
opts registry.Options
// connect enabled
connect bool
queryOptions *consul.QueryOptions
sync.Mutex
register map[string]uint64
// lastChecked tracks when a node was last checked as existing in Consul
lastChecked map[string]time.Time
}
func getDeregisterTTL(t time.Duration) time.Duration {
// splay slightly for the watcher?
splay := time.Second * 5
deregTTL := t + splay
// consul has a minimum timeout on deregistration of 1 minute.
if t < time.Minute {
deregTTL = time.Minute + splay
}
return deregTTL
}
func newTransport(config *tls.Config) *http.Transport {
if config == nil {
config = &tls.Config{
InsecureSkipVerify: true,
}
}
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: config,
}
runtime.SetFinalizer(&t, func(tr **http.Transport) {
(*tr).CloseIdleConnections()
})
return t
}
func configure(c *consulRegistry, opts ...registry.Option) {
// set opts
for _, o := range opts {
o(&c.opts)
}
// use default config
config := consul.DefaultConfig()
if c.opts.Context != nil {
// Use the consul config passed in the options, if available
if co, ok := c.opts.Context.Value("consul_config").(*consul.Config); ok {
config = co
}
if cn, ok := c.opts.Context.Value("consul_connect").(bool); ok {
c.connect = cn
}
// Use the consul query options passed in the options, if available
if qo, ok := c.opts.Context.Value("consul_query_options").(*consul.QueryOptions); ok && qo != nil {
c.queryOptions = qo
}
if as, ok := c.opts.Context.Value("consul_allow_stale").(bool); ok {
c.queryOptions.AllowStale = as
}
}
// check if there are any addrs
if len(c.opts.Addrs) > 0 {
addr, port, err := net.SplitHostPort(c.opts.Addrs[0])
if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" {
port = "8500"
addr = c.opts.Addrs[0]
config.Address = fmt.Sprintf("%s:%s", addr, port)
} else if err == nil {
config.Address = fmt.Sprintf("%s:%s", addr, port)
}
}
// requires secure connection?
if c.opts.Secure || c.opts.TLSConfig != nil {
if config.HttpClient == nil {
config.HttpClient = new(http.Client)
}
config.Scheme = "https"
// We're going to support InsecureSkipVerify
config.HttpClient.Transport = newTransport(c.opts.TLSConfig)
}
// set timeout
if c.opts.Timeout > 0 {
config.HttpClient.Timeout = c.opts.Timeout
}
// create the client
client, _ := consul.NewClient(config)
// set address/client
c.Address = config.Address
c.Client = client
}
func (c *consulRegistry) Init(opts ...registry.Option) error {
configure(c, opts...)
return nil
}
func (c *consulRegistry) Deregister(s *registry.Service) error {
if len(s.Nodes) == 0 {
return errors.New("Require at least one node")
}
// delete our hash and time check of the service
c.Lock()
delete(c.register, s.Name)
delete(c.lastChecked, s.Name)
c.Unlock()
node := s.Nodes[0]
return c.Client.Agent().ServiceDeregister(node.Id)
}
func (c *consulRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
if len(s.Nodes) == 0 {
return errors.New("Require at least one node")
}
var regTCPCheck bool
var regInterval time.Duration
var options registry.RegisterOptions
for _, o := range opts {
o(&options)
}
if c.opts.Context != nil {
if tcpCheckInterval, ok := c.opts.Context.Value("consul_tcp_check").(time.Duration); ok {
regTCPCheck = true
regInterval = tcpCheckInterval
}
}
// create hash of service; uint64
h, err := hash.Hash(s, nil)
if err != nil {
return err
}
// use first node
node := s.Nodes[0]
// get existing hash and last checked time
c.Lock()
v, ok := c.register[s.Name]
lastChecked := c.lastChecked[s.Name]
c.Unlock()
// if it's already registered and matches then just pass the check
if ok && v == h {
if options.TTL == time.Duration(0) {
// ensure that our service hasn't been deregistered by Consul
if time.Since(lastChecked) <= getDeregisterTTL(regInterval) {
return nil
}
services, _, err := c.Client.Health().Checks(s.Name, c.queryOptions)
if err == nil {
for _, v := range services {
if v.ServiceID == node.Id {
return nil
}
}
}
} else {
// if the err is nil we're all good, bail out
// if not, we don't know what the state is, so full re-register
if err := c.Client.Agent().PassTTL("service:"+node.Id, ""); err == nil {
return nil
}
}
}
// encode the tags
tags := encodeMetadata(node.Metadata)
tags = append(tags, encodeEndpoints(s.Endpoints)...)
tags = append(tags, encodeVersion(s.Version)...)
var check *consul.AgentServiceCheck
if regTCPCheck {
deregTTL := getDeregisterTTL(regInterval)
check = &consul.AgentServiceCheck{
TCP: fmt.Sprintf("%s:%d", node.Address, node.Port),
Interval: fmt.Sprintf("%v", regInterval),
DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
}
// if the TTL is greater than 0 create an associated check
} else if options.TTL > time.Duration(0) {
deregTTL := getDeregisterTTL(options.TTL)
check = &consul.AgentServiceCheck{
TTL: fmt.Sprintf("%v", options.TTL),
DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
}
}
// register the service
asr := &consul.AgentServiceRegistration{
ID: node.Id,
Name: s.Name,
Tags: tags,
Port: node.Port,
Address: node.Address,
Check: check,
}
// Specify consul connect
if c.connect {
asr.Connect = &consul.AgentServiceConnect{
Native: true,
}
}
if err := c.Client.Agent().ServiceRegister(asr); err != nil {
return err
}
// save our hash and time check of the service
c.Lock()
c.register[s.Name] = h
c.lastChecked[s.Name] = time.Now()
c.Unlock()
// if the TTL is 0 we don't mess with the checks
if options.TTL == time.Duration(0) {
return nil
}
// pass the healthcheck
return c.Client.Agent().PassTTL("service:"+node.Id, "")
}
func (c *consulRegistry) GetService(name string) ([]*registry.Service, error) {
var rsp []*consul.ServiceEntry
var err error
// if we're connect enabled only get connect services
if c.connect {
rsp, _, err = c.Client.Health().Connect(name, "", false, c.queryOptions)
} else {
rsp, _, err = c.Client.Health().Service(name, "", false, c.queryOptions)
}
if err != nil {
return nil, err
}
serviceMap := map[string]*registry.Service{}
for _, s := range rsp {
if s.Service.Service != name {
continue
}
// version is now a tag
version, _ := decodeVersion(s.Service.Tags)
// service ID is now the node id
id := s.Service.ID
// key is always the version
key := version
// address is service address
address := s.Service.Address
// use node address
if len(address) == 0 {
address = s.Node.Address
}
svc, ok := serviceMap[key]
if !ok {
svc = &registry.Service{
Endpoints: decodeEndpoints(s.Service.Tags),
Name: s.Service.Service,
Version: version,
}
serviceMap[key] = svc
}
var del bool
for _, check := range s.Checks {
// delete the node if the status is critical
if check.Status == "critical" {
del = true
break
}
}
// if delete then skip the node
if del {
continue
}
svc.Nodes = append(svc.Nodes, &registry.Node{
Id: id,
Address: address,
Port: s.Service.Port,
Metadata: decodeMetadata(s.Service.Tags),
})
}
var services []*registry.Service
for _, service := range serviceMap {
services = append(services, service)
}
return services, nil
}
func (c *consulRegistry) ListServices() ([]*registry.Service, error) {
rsp, _, err := c.Client.Catalog().Services(c.queryOptions)
if err != nil {
return nil, err
}
var services []*registry.Service
for service := range rsp {
services = append(services, &registry.Service{Name: service})
}
return services, nil
}
func (c *consulRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
return newConsulWatcher(c, opts...)
}
func (c *consulRegistry) String() string {
return "consul"
}
func (c *consulRegistry) Options() registry.Options {
return c.opts
}
func NewRegistry(opts ...registry.Option) registry.Registry {
cr := &consulRegistry{
opts: registry.Options{},
register: make(map[string]uint64),
lastChecked: make(map[string]time.Time),
queryOptions: &consul.QueryOptions{
AllowStale: true,
},
}
configure(cr, opts...)
return cr
}

170
registry/consul/encoding.go Normal file
View File

@@ -0,0 +1,170 @@
package consul
import (
"bytes"
"compress/zlib"
"encoding/hex"
"encoding/json"
"io/ioutil"
"github.com/micro/go-micro/registry"
)
func encode(buf []byte) string {
var b bytes.Buffer
defer b.Reset()
w := zlib.NewWriter(&b)
if _, err := w.Write(buf); err != nil {
return ""
}
w.Close()
return hex.EncodeToString(b.Bytes())
}
func decode(d string) []byte {
hr, err := hex.DecodeString(d)
if err != nil {
return nil
}
br := bytes.NewReader(hr)
zr, err := zlib.NewReader(br)
if err != nil {
return nil
}
rbuf, err := ioutil.ReadAll(zr)
if err != nil {
return nil
}
return rbuf
}
func encodeEndpoints(en []*registry.Endpoint) []string {
var tags []string
for _, e := range en {
if b, err := json.Marshal(e); err == nil {
tags = append(tags, "e-"+encode(b))
}
}
return tags
}
func decodeEndpoints(tags []string) []*registry.Endpoint {
var en []*registry.Endpoint
// use the first format you find
var ver byte
for _, tag := range tags {
if len(tag) == 0 || tag[0] != 'e' {
continue
}
// check version
if ver > 0 && tag[1] != ver {
continue
}
var e *registry.Endpoint
var buf []byte
// Old encoding was plain
if tag[1] == '=' {
buf = []byte(tag[2:])
}
// New encoding is hex
if tag[1] == '-' {
buf = decode(tag[2:])
}
if err := json.Unmarshal(buf, &e); err == nil {
en = append(en, e)
}
// set version
ver = tag[1]
}
return en
}
func encodeMetadata(md map[string]string) []string {
var tags []string
for k, v := range md {
if b, err := json.Marshal(map[string]string{
k: v,
}); err == nil {
// new encoding
tags = append(tags, "t-"+encode(b))
}
}
return tags
}
func decodeMetadata(tags []string) map[string]string {
md := make(map[string]string)
var ver byte
for _, tag := range tags {
if len(tag) == 0 || tag[0] != 't' {
continue
}
// check version
if ver > 0 && tag[1] != ver {
continue
}
var kv map[string]string
var buf []byte
// Old encoding was plain
if tag[1] == '=' {
buf = []byte(tag[2:])
}
// New encoding is hex
if tag[1] == '-' {
buf = decode(tag[2:])
}
// Now unmarshal
if err := json.Unmarshal(buf, &kv); err == nil {
for k, v := range kv {
md[k] = v
}
}
// set version
ver = tag[1]
}
return md
}
func encodeVersion(v string) []string {
return []string{"v-" + encode([]byte(v))}
}
func decodeVersion(tags []string) (string, bool) {
for _, tag := range tags {
if len(tag) < 2 || tag[0] != 'v' {
continue
}
// Old encoding was plain
if tag[1] == '=' {
return tag[2:], true
}
// New encoding is hex
if tag[1] == '-' {
return string(decode(tag[2:])), true
}
}
return "", false
}

View File

@@ -0,0 +1,147 @@
package consul
import (
"encoding/json"
"testing"
"github.com/micro/go-micro/registry"
)
func TestEncodingEndpoints(t *testing.T) {
eps := []*registry.Endpoint{
&registry.Endpoint{
Name: "endpoint1",
Request: &registry.Value{
Name: "request",
Type: "request",
},
Response: &registry.Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo1": "bar1",
},
},
&registry.Endpoint{
Name: "endpoint2",
Request: &registry.Value{
Name: "request",
Type: "request",
},
Response: &registry.Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo2": "bar2",
},
},
&registry.Endpoint{
Name: "endpoint3",
Request: &registry.Value{
Name: "request",
Type: "request",
},
Response: &registry.Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo3": "bar3",
},
},
}
testEp := func(ep *registry.Endpoint, enc string) {
// encode endpoint
e := encodeEndpoints([]*registry.Endpoint{ep})
// check there are two tags; old and new
if len(e) != 1 {
t.Fatalf("Expected 1 encoded tags, got %v", e)
}
// check old encoding
var seen bool
for _, en := range e {
if en == enc {
seen = true
break
}
}
if !seen {
t.Fatalf("Expected %s but not found", enc)
}
// decode
d := decodeEndpoints([]string{enc})
if len(d) == 0 {
t.Fatalf("Expected %v got %v", ep, d)
}
// check name
if d[0].Name != ep.Name {
t.Fatalf("Expected ep %s got %s", ep.Name, d[0].Name)
}
// check all the metadata exists
for k, v := range ep.Metadata {
if gv := d[0].Metadata[k]; gv != v {
t.Fatalf("Expected key %s val %s got val %s", k, v, gv)
}
}
}
for _, ep := range eps {
// JSON encoded
jencoded, err := json.Marshal(ep)
if err != nil {
t.Fatal(err)
}
// HEX encoded
hencoded := encode(jencoded)
// endpoint tag
hepTag := "e-" + hencoded
testEp(ep, hepTag)
}
}
func TestEncodingVersion(t *testing.T) {
testData := []struct {
decoded string
encoded string
}{
{"1.0.0", "v-789c32d433d03300040000ffff02ce00ee"},
{"latest", "v-789cca492c492d2e01040000ffff08cc028e"},
}
for _, data := range testData {
e := encodeVersion(data.decoded)
if e[0] != data.encoded {
t.Fatalf("Expected %s got %s", data.encoded, e)
}
d, ok := decodeVersion(e)
if !ok {
t.Fatalf("Unexpected %t for %s", ok, data.encoded)
}
if d != data.decoded {
t.Fatalf("Expected %s got %s", data.decoded, d)
}
d, ok = decodeVersion([]string{data.encoded})
if !ok {
t.Fatalf("Unexpected %t for %s", ok, data.encoded)
}
if d != data.decoded {
t.Fatalf("Expected %s got %s", data.decoded, d)
}
}
}

View File

@@ -1,4 +1,4 @@
package registry
package consul
import (
"bytes"
@@ -10,6 +10,7 @@ import (
"time"
consul "github.com/hashicorp/consul/api"
"github.com/micro/go-micro/registry"
)
type mockRegistry struct {
@@ -56,7 +57,7 @@ func newConsulTestRegistry(r *mockRegistry) (*consulRegistry, func()) {
return &consulRegistry{
Address: cfg.Address,
Client: cl,
opts: Options{},
opts: registry.Options{},
register: make(map[string]uint64),
lastChecked: make(map[string]time.Time),
queryOptions: &consul.QueryOptions{

View File

@@ -1,4 +1,4 @@
package registry
package consul
import (
"errors"
@@ -6,23 +6,24 @@ import (
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/watch"
"github.com/micro/go-micro/registry"
)
type consulWatcher struct {
r *consulRegistry
wo WatchOptions
wo registry.WatchOptions
wp *watch.Plan
watchers map[string]*watch.Plan
next chan *Result
next chan *registry.Result
exit chan bool
sync.RWMutex
services map[string][]*Service
services map[string][]*registry.Service
}
func newConsulWatcher(cr *consulRegistry, opts ...WatchOption) (Watcher, error) {
var wo WatchOptions
func newConsulWatcher(cr *consulRegistry, opts ...registry.WatchOption) (registry.Watcher, error) {
var wo registry.WatchOptions
for _, o := range opts {
o(&wo)
}
@@ -31,9 +32,9 @@ func newConsulWatcher(cr *consulRegistry, opts ...WatchOption) (Watcher, error)
r: cr,
wo: wo,
exit: make(chan bool),
next: make(chan *Result, 10),
next: make(chan *registry.Result, 10),
watchers: make(map[string]*watch.Plan),
services: make(map[string][]*Service),
services: make(map[string][]*registry.Service),
}
wp, err := watch.Parse(map[string]interface{}{"type": "services"})
@@ -54,7 +55,7 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
return
}
serviceMap := map[string]*Service{}
serviceMap := map[string]*registry.Service{}
serviceName := ""
for _, e := range entries {
@@ -75,7 +76,7 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
svc, ok := serviceMap[key]
if !ok {
svc = &Service{
svc = &registry.Service{
Endpoints: decodeEndpoints(e.Service.Tags),
Name: e.Service.Service,
Version: version,
@@ -98,7 +99,7 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
continue
}
svc.Nodes = append(svc.Nodes, &Node{
svc.Nodes = append(svc.Nodes, &registry.Node{
Id: id,
Address: address,
Port: e.Service.Port,
@@ -108,13 +109,13 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
cw.RLock()
// make a copy
rservices := make(map[string][]*Service)
rservices := make(map[string][]*registry.Service)
for k, v := range cw.services {
rservices[k] = v
}
cw.RUnlock()
var newServices []*Service
var newServices []*registry.Service
// serviceMap is the new set of services keyed by name+version
for _, newService := range serviceMap {
@@ -125,7 +126,7 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
oldServices, ok := rservices[serviceName]
if !ok {
// does not exist? then we're creating brand new entries
cw.next <- &Result{Action: "create", Service: newService}
cw.next <- &registry.Result{Action: "create", Service: newService}
continue
}
@@ -142,7 +143,7 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
// yes? then it's an update
action = "update"
var nodes []*Node
var nodes []*registry.Node
// check the old nodes to see if they've been deleted
for _, oldNode := range oldService.Nodes {
var seen bool
@@ -163,11 +164,11 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
if len(nodes) > 0 {
delService := oldService
delService.Nodes = nodes
cw.next <- &Result{Action: "delete", Service: delService}
cw.next <- &registry.Result{Action: "delete", Service: delService}
}
}
cw.next <- &Result{Action: action, Service: newService}
cw.next <- &registry.Result{Action: action, Service: newService}
}
// Now check old versions that may not be in new services map
@@ -175,7 +176,7 @@ func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) {
// old version does not exist in new version map
// kill it with fire!
if _, ok := serviceMap[old.Version]; !ok {
cw.next <- &Result{Action: "delete", Service: old}
cw.next <- &registry.Result{Action: "delete", Service: old}
}
}
@@ -209,13 +210,13 @@ func (cw *consulWatcher) handle(idx uint64, data interface{}) {
wp.Handler = cw.serviceHandler
go wp.Run(cw.r.Address)
cw.watchers[service] = wp
cw.next <- &Result{Action: "create", Service: &Service{Name: service}}
cw.next <- &registry.Result{Action: "create", Service: &registry.Service{Name: service}}
}
}
cw.RLock()
// make a copy
rservices := make(map[string][]*Service)
rservices := make(map[string][]*registry.Service)
for k, v := range cw.services {
rservices[k] = v
}
@@ -235,12 +236,12 @@ func (cw *consulWatcher) handle(idx uint64, data interface{}) {
if _, ok := services[service]; !ok {
w.Stop()
delete(cw.watchers, service)
cw.next <- &Result{Action: "delete", Service: &Service{Name: service}}
cw.next <- &registry.Result{Action: "delete", Service: &registry.Service{Name: service}}
}
}
}
func (cw *consulWatcher) Next() (*Result, error) {
func (cw *consulWatcher) Next() (*registry.Result, error) {
select {
case <-cw.exit:
return nil, errors.New("result chan closed")

View File

@@ -1,9 +1,10 @@
package registry
package consul
import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/micro/go-micro/registry"
)
func TestHealthyServiceHandler(t *testing.T) {
@@ -58,8 +59,8 @@ func TestUnhealthyNodeServiceHandler(t *testing.T) {
func newWatcher() *consulWatcher {
return &consulWatcher{
exit: make(chan bool),
next: make(chan *Result, 10),
services: make(map[string][]*Service),
next: make(chan *registry.Result, 10),
services: make(map[string][]*registry.Service),
}
}

View File

@@ -1,386 +0,0 @@
package registry
import (
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"runtime"
"sync"
"time"
consul "github.com/hashicorp/consul/api"
hash "github.com/mitchellh/hashstructure"
)
type consulRegistry struct {
Address string
Client *consul.Client
opts Options
// connect enabled
connect bool
queryOptions *consul.QueryOptions
sync.Mutex
register map[string]uint64
// lastChecked tracks when a node was last checked as existing in Consul
lastChecked map[string]time.Time
}
func getDeregisterTTL(t time.Duration) time.Duration {
// splay slightly for the watcher?
splay := time.Second * 5
deregTTL := t + splay
// consul has a minimum timeout on deregistration of 1 minute.
if t < time.Minute {
deregTTL = time.Minute + splay
}
return deregTTL
}
func newTransport(config *tls.Config) *http.Transport {
if config == nil {
config = &tls.Config{
InsecureSkipVerify: true,
}
}
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: config,
}
runtime.SetFinalizer(&t, func(tr **http.Transport) {
(*tr).CloseIdleConnections()
})
return t
}
func configure(c *consulRegistry, opts ...Option) {
// set opts
for _, o := range opts {
o(&c.opts)
}
// use default config
config := consul.DefaultConfig()
if c.opts.Context != nil {
// Use the consul config passed in the options, if available
if co, ok := c.opts.Context.Value("consul_config").(*consul.Config); ok {
config = co
}
if cn, ok := c.opts.Context.Value("consul_connect").(bool); ok {
c.connect = cn
}
// Use the consul query options passed in the options, if available
if qo, ok := c.opts.Context.Value("consul_query_options").(*consul.QueryOptions); ok && qo != nil {
c.queryOptions = qo
}
if as, ok := c.opts.Context.Value("consul_allow_stale").(bool); ok {
c.queryOptions.AllowStale = as
}
}
// check if there are any addrs
if len(c.opts.Addrs) > 0 {
addr, port, err := net.SplitHostPort(c.opts.Addrs[0])
if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" {
port = "8500"
addr = c.opts.Addrs[0]
config.Address = fmt.Sprintf("%s:%s", addr, port)
} else if err == nil {
config.Address = fmt.Sprintf("%s:%s", addr, port)
}
}
// requires secure connection?
if c.opts.Secure || c.opts.TLSConfig != nil {
if config.HttpClient == nil {
config.HttpClient = new(http.Client)
}
config.Scheme = "https"
// We're going to support InsecureSkipVerify
config.HttpClient.Transport = newTransport(c.opts.TLSConfig)
}
// set timeout
if c.opts.Timeout > 0 {
config.HttpClient.Timeout = c.opts.Timeout
}
// create the client
client, _ := consul.NewClient(config)
// set address/client
c.Address = config.Address
c.Client = client
}
func newConsulRegistry(opts ...Option) Registry {
cr := &consulRegistry{
opts: Options{},
register: make(map[string]uint64),
lastChecked: make(map[string]time.Time),
queryOptions: &consul.QueryOptions{
AllowStale: true,
},
}
configure(cr, opts...)
return cr
}
func (c *consulRegistry) Init(opts ...Option) error {
configure(c, opts...)
return nil
}
func (c *consulRegistry) Deregister(s *Service) error {
if len(s.Nodes) == 0 {
return errors.New("Require at least one node")
}
// delete our hash and time check of the service
c.Lock()
delete(c.register, s.Name)
delete(c.lastChecked, s.Name)
c.Unlock()
node := s.Nodes[0]
return c.Client.Agent().ServiceDeregister(node.Id)
}
func (c *consulRegistry) Register(s *Service, opts ...RegisterOption) error {
if len(s.Nodes) == 0 {
return errors.New("Require at least one node")
}
var regTCPCheck bool
var regInterval time.Duration
var options RegisterOptions
for _, o := range opts {
o(&options)
}
if c.opts.Context != nil {
if tcpCheckInterval, ok := c.opts.Context.Value("consul_tcp_check").(time.Duration); ok {
regTCPCheck = true
regInterval = tcpCheckInterval
}
}
// create hash of service; uint64
h, err := hash.Hash(s, nil)
if err != nil {
return err
}
// use first node
node := s.Nodes[0]
// get existing hash and last checked time
c.Lock()
v, ok := c.register[s.Name]
lastChecked := c.lastChecked[s.Name]
c.Unlock()
// if it's already registered and matches then just pass the check
if ok && v == h {
if options.TTL == time.Duration(0) {
// ensure that our service hasn't been deregistered by Consul
if time.Since(lastChecked) <= getDeregisterTTL(regInterval) {
return nil
}
services, _, err := c.Client.Health().Checks(s.Name, c.queryOptions)
if err == nil {
for _, v := range services {
if v.ServiceID == node.Id {
return nil
}
}
}
} else {
// if the err is nil we're all good, bail out
// if not, we don't know what the state is, so full re-register
if err := c.Client.Agent().PassTTL("service:"+node.Id, ""); err == nil {
return nil
}
}
}
// encode the tags
tags := encodeMetadata(node.Metadata)
tags = append(tags, encodeEndpoints(s.Endpoints)...)
tags = append(tags, encodeVersion(s.Version)...)
var check *consul.AgentServiceCheck
if regTCPCheck {
deregTTL := getDeregisterTTL(regInterval)
check = &consul.AgentServiceCheck{
TCP: fmt.Sprintf("%s:%d", node.Address, node.Port),
Interval: fmt.Sprintf("%v", regInterval),
DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
}
// if the TTL is greater than 0 create an associated check
} else if options.TTL > time.Duration(0) {
deregTTL := getDeregisterTTL(options.TTL)
check = &consul.AgentServiceCheck{
TTL: fmt.Sprintf("%v", options.TTL),
DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
}
}
// register the service
asr := &consul.AgentServiceRegistration{
ID: node.Id,
Name: s.Name,
Tags: tags,
Port: node.Port,
Address: node.Address,
Check: check,
}
// Specify consul connect
if c.connect {
asr.Connect = &consul.AgentServiceConnect{
Native: true,
}
}
if err := c.Client.Agent().ServiceRegister(asr); err != nil {
return err
}
// save our hash and time check of the service
c.Lock()
c.register[s.Name] = h
c.lastChecked[s.Name] = time.Now()
c.Unlock()
// if the TTL is 0 we don't mess with the checks
if options.TTL == time.Duration(0) {
return nil
}
// pass the healthcheck
return c.Client.Agent().PassTTL("service:"+node.Id, "")
}
func (c *consulRegistry) GetService(name string) ([]*Service, error) {
var rsp []*consul.ServiceEntry
var err error
// if we're connect enabled only get connect services
if c.connect {
rsp, _, err = c.Client.Health().Connect(name, "", false, c.queryOptions)
} else {
rsp, _, err = c.Client.Health().Service(name, "", false, c.queryOptions)
}
if err != nil {
return nil, err
}
serviceMap := map[string]*Service{}
for _, s := range rsp {
if s.Service.Service != name {
continue
}
// version is now a tag
version, _ := decodeVersion(s.Service.Tags)
// service ID is now the node id
id := s.Service.ID
// key is always the version
key := version
// address is service address
address := s.Service.Address
// use node address
if len(address) == 0 {
address = s.Node.Address
}
svc, ok := serviceMap[key]
if !ok {
svc = &Service{
Endpoints: decodeEndpoints(s.Service.Tags),
Name: s.Service.Service,
Version: version,
}
serviceMap[key] = svc
}
var del bool
for _, check := range s.Checks {
// delete the node if the status is critical
if check.Status == "critical" {
del = true
break
}
}
// if delete then skip the node
if del {
continue
}
svc.Nodes = append(svc.Nodes, &Node{
Id: id,
Address: address,
Port: s.Service.Port,
Metadata: decodeMetadata(s.Service.Tags),
})
}
var services []*Service
for _, service := range serviceMap {
services = append(services, service)
}
return services, nil
}
func (c *consulRegistry) ListServices() ([]*Service, error) {
rsp, _, err := c.Client.Catalog().Services(c.queryOptions)
if err != nil {
return nil, err
}
var services []*Service
for service := range rsp {
services = append(services, &Service{Name: service})
}
return services, nil
}
func (c *consulRegistry) Watch(opts ...WatchOption) (Watcher, error) {
return newConsulWatcher(c, opts...)
}
func (c *consulRegistry) String() string {
return "consul"
}
func (c *consulRegistry) Options() Options {
return c.opts
}

View File

@@ -6,163 +6,68 @@ import (
"encoding/hex"
"encoding/json"
"io/ioutil"
"strings"
)
func encode(buf []byte) string {
var b bytes.Buffer
defer b.Reset()
func encode(txt *mdnsTxt) ([]string, error) {
b, err := json.Marshal(txt)
if err != nil {
return nil, err
}
w := zlib.NewWriter(&b)
if _, err := w.Write(buf); err != nil {
return ""
var buf bytes.Buffer
defer buf.Reset()
w := zlib.NewWriter(&buf)
if _, err := w.Write(b); err != nil {
return nil, err
}
w.Close()
return hex.EncodeToString(b.Bytes())
encoded := hex.EncodeToString(buf.Bytes())
// individual txt limit
if len(encoded) <= 255 {
return []string{encoded}, nil
}
// split encoded string
var record []string
for len(encoded) > 255 {
record = append(record, encoded[:255])
encoded = encoded[255:]
}
record = append(record, encoded)
return record, nil
}
func decode(d string) []byte {
hr, err := hex.DecodeString(d)
func decode(record []string) (*mdnsTxt, error) {
encoded := strings.Join(record, "")
hr, err := hex.DecodeString(encoded)
if err != nil {
return nil
return nil, err
}
br := bytes.NewReader(hr)
zr, err := zlib.NewReader(br)
if err != nil {
return nil
return nil, err
}
rbuf, err := ioutil.ReadAll(zr)
if err != nil {
return nil
return nil, err
}
return rbuf
}
var txt *mdnsTxt
func encodeEndpoints(en []*Endpoint) []string {
var tags []string
for _, e := range en {
if b, err := json.Marshal(e); err == nil {
tags = append(tags, "e-"+encode(b))
}
if err := json.Unmarshal(rbuf, &txt); err != nil {
return nil, err
}
return tags
}
func decodeEndpoints(tags []string) []*Endpoint {
var en []*Endpoint
// use the first format you find
var ver byte
for _, tag := range tags {
if len(tag) == 0 || tag[0] != 'e' {
continue
}
// check version
if ver > 0 && tag[1] != ver {
continue
}
var e *Endpoint
var buf []byte
// Old encoding was plain
if tag[1] == '=' {
buf = []byte(tag[2:])
}
// New encoding is hex
if tag[1] == '-' {
buf = decode(tag[2:])
}
if err := json.Unmarshal(buf, &e); err == nil {
en = append(en, e)
}
// set version
ver = tag[1]
}
return en
}
func encodeMetadata(md map[string]string) []string {
var tags []string
for k, v := range md {
if b, err := json.Marshal(map[string]string{
k: v,
}); err == nil {
// new encoding
tags = append(tags, "t-"+encode(b))
}
}
return tags
}
func decodeMetadata(tags []string) map[string]string {
md := make(map[string]string)
var ver byte
for _, tag := range tags {
if len(tag) == 0 || tag[0] != 't' {
continue
}
// check version
if ver > 0 && tag[1] != ver {
continue
}
var kv map[string]string
var buf []byte
// Old encoding was plain
if tag[1] == '=' {
buf = []byte(tag[2:])
}
// New encoding is hex
if tag[1] == '-' {
buf = decode(tag[2:])
}
// Now unmarshal
if err := json.Unmarshal(buf, &kv); err == nil {
for k, v := range kv {
md[k] = v
}
}
// set version
ver = tag[1]
}
return md
}
func encodeVersion(v string) []string {
return []string{"v-" + encode([]byte(v))}
}
func decodeVersion(tags []string) (string, bool) {
for _, tag := range tags {
if len(tag) < 2 || tag[0] != 'v' {
continue
}
// Old encoding was plain
if tag[1] == '=' {
return tag[2:], true
}
// New encoding is hex
if tag[1] == '-' {
return string(decode(tag[2:])), true
}
}
return "", false
return txt, nil
}

View File

@@ -1,146 +1,65 @@
package registry
import (
"encoding/json"
"testing"
)
func TestEncodingEndpoints(t *testing.T) {
eps := []*Endpoint{
&Endpoint{
Name: "endpoint1",
Request: &Value{
Name: "request",
Type: "request",
},
Response: &Value{
Name: "response",
Type: "response",
},
func TestEncoding(t *testing.T) {
testData := []*mdnsTxt{
&mdnsTxt{
Version: "1.0.0",
Metadata: map[string]string{
"foo1": "bar1",
"foo": "bar",
},
},
&Endpoint{
Name: "endpoint2",
Request: &Value{
Name: "request",
Type: "request",
},
Response: &Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo2": "bar2",
},
},
&Endpoint{
Name: "endpoint3",
Request: &Value{
Name: "request",
Type: "request",
},
Response: &Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo3": "bar3",
Endpoints: []*Endpoint{
&Endpoint{
Name: "endpoint1",
Request: &Value{
Name: "request",
Type: "request",
},
Response: &Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo1": "bar1",
},
},
},
},
}
testEp := func(ep *Endpoint, enc string) {
// encode endpoint
e := encodeEndpoints([]*Endpoint{ep})
// check there are two tags; old and new
if len(e) != 1 {
t.Fatalf("Expected 1 encoded tags, got %v", e)
}
// check old encoding
var seen bool
for _, en := range e {
if en == enc {
seen = true
break
}
}
if !seen {
t.Fatalf("Expected %s but not found", enc)
}
// decode
d := decodeEndpoints([]string{enc})
if len(d) == 0 {
t.Fatalf("Expected %v got %v", ep, d)
}
// check name
if d[0].Name != ep.Name {
t.Fatalf("Expected ep %s got %s", ep.Name, d[0].Name)
}
// check all the metadata exists
for k, v := range ep.Metadata {
if gv := d[0].Metadata[k]; gv != v {
t.Fatalf("Expected key %s val %s got val %s", k, v, gv)
}
}
}
for _, ep := range eps {
// JSON encoded
jencoded, err := json.Marshal(ep)
for _, d := range testData {
encoded, err := encode(d)
if err != nil {
t.Fatal(err)
}
// HEX encoded
hencoded := encode(jencoded)
// endpoint tag
hepTag := "e-" + hencoded
testEp(ep, hepTag)
}
}
func TestEncodingVersion(t *testing.T) {
testData := []struct {
decoded string
encoded string
}{
{"1.0.0", "v-789c32d433d03300040000ffff02ce00ee"},
{"latest", "v-789cca492c492d2e01040000ffff08cc028e"},
}
for _, data := range testData {
e := encodeVersion(data.decoded)
if e[0] != data.encoded {
t.Fatalf("Expected %s got %s", data.encoded, e)
}
d, ok := decodeVersion(e)
if !ok {
t.Fatalf("Unexpected %t for %s", ok, data.encoded)
}
if d != data.decoded {
t.Fatalf("Expected %s got %s", data.decoded, d)
}
d, ok = decodeVersion([]string{data.encoded})
if !ok {
t.Fatalf("Unexpected %t for %s", ok, data.encoded)
}
if d != data.decoded {
t.Fatalf("Expected %s got %s", data.decoded, d)
}
for _, txt := range encoded {
if len(txt) > 255 {
t.Fatalf("One of parts for txt is %d characters", len(txt))
}
}
decoded, err := decode(encoded)
if err != nil {
t.Fatal(err)
}
if decoded.Version != d.Version {
t.Fatalf("Expected version %s got %s", d.Version, decoded.Version)
}
if len(decoded.Endpoints) != len(d.Endpoints) {
t.Fatalf("Expected %d endpoints, got %d", len(d.Endpoints), len(decoded.Endpoints))
}
for k, v := range d.Metadata {
if val := decoded.Metadata[k]; val != v {
t.Fatalf("Expected %s=%s got %s=%s", k, v, k, val)
}
}
}
}

View File

@@ -1,6 +1,6 @@
# Gossip Registry
Gossip is a zero dependency registry which uses hashicorp/memberlist to broadcast registry information
Gossip is a zero dependency registry which uses github.com/hashicorp/memberlist to broadcast registry information
via the SWIM protocol.
## Usage
@@ -20,5 +20,5 @@ On startup you'll see something like
To join this gossip ring set the registry address using flag or env var
```bash
MICRO_REGISTRY_ADDRESS= 192.168.1.65:56390
MICRO_REGISTRY_ADDRESS=192.168.1.65:56390
```

View File

@@ -0,0 +1,17 @@
package gossip
import (
"context"
"github.com/micro/go-micro/registry"
)
// setRegistryOption returns a function to setup a context with given value
func setRegistryOption(k, v interface{}) registry.Option {
return func(o *registry.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, k, v)
}
}

View File

@@ -4,12 +4,15 @@ package gossip
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/golang/protobuf/proto"
@@ -21,10 +24,42 @@ import (
"github.com/mitchellh/hashstructure"
)
// use registry.Result int32 values after it switches from string to int32 types
// type actionType int32
// type updateType int32
const (
addAction = "update"
delAction = "delete"
syncAction = "sync"
actionTypeInvalid int32 = iota
actionTypeCreate
actionTypeDelete
actionTypeUpdate
actionTypeSync
)
const (
nodeActionUnknown int32 = iota
nodeActionJoin
nodeActionLeave
nodeActionUpdate
)
func actionTypeString(t int32) string {
switch t {
case actionTypeCreate:
return "create"
case actionTypeDelete:
return "delete"
case actionTypeUpdate:
return "update"
case actionTypeSync:
return "sync"
}
return "invalid"
}
const (
updateTypeInvalid int32 = iota
updateTypeService
)
type broadcast struct {
@@ -37,18 +72,45 @@ type delegate struct {
updates chan *update
}
type gossipRegistry struct {
queue *memberlist.TransmitLimitedQueue
updates chan *update
options registry.Options
member *memberlist.Memberlist
interval time.Duration
type event struct {
action int32
node string
}
type eventDelegate struct {
events chan *event
}
func (ed *eventDelegate) NotifyJoin(n *memberlist.Node) {
ed.events <- &event{action: nodeActionJoin, node: n.Address()}
}
func (ed *eventDelegate) NotifyLeave(n *memberlist.Node) {
ed.events <- &event{action: nodeActionLeave, node: n.Address()}
}
func (ed *eventDelegate) NotifyUpdate(n *memberlist.Node) {
ed.events <- &event{action: nodeActionUpdate, node: n.Address()}
}
type gossipRegistry struct {
queue *memberlist.TransmitLimitedQueue
updates chan *update
events chan *event
options registry.Options
member *memberlist.Memberlist
interval time.Duration
tcpInterval time.Duration
connectRetry bool
connectTimeout time.Duration
sync.RWMutex
services map[string][]*registry.Service
s sync.RWMutex
watchers map[string]chan *registry.Result
mtu int
addrs []string
members map[string]int32
done chan struct{}
}
type update struct {
@@ -60,9 +122,60 @@ type update struct {
var (
// You should change this if using secure
DefaultSecret = []byte("micro-gossip-key") // exactly 16 bytes
ExpiryTick = time.Second * 5
ExpiryTick = time.Second * 1 // needs to be smaller than registry.RegisterTTL
MaxPacketSize = 512
)
func (g *gossipRegistry) connect(addrs []string) error {
var err error
if len(addrs) == 0 {
return nil
}
timeout := make(<-chan time.Time)
if g.connectTimeout > 0 {
timeout = time.After(g.connectTimeout)
}
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
fn := func() (int, error) {
return g.member.Join(addrs)
}
// don't wait for first try
if _, err = fn(); err == nil {
return nil
}
// wait loop
for {
select {
// context closed
case <-g.options.Context.Done():
return nil
// call close, don't wait anymore
case <-g.done:
return nil
// in case of timeout fail with a timeout error
case <-timeout:
return fmt.Errorf("[gossip]: timedout connect to %v", g.addrs)
// got a tick, try to connect
case <-ticker.C:
if _, err = fn(); err == nil {
log.Logf("[gossip]: success connect to %v", g.addrs)
return nil
} else {
log.Logf("[gossip]: failed connect to %v", g.addrs)
}
}
}
return err
}
func configure(g *gossipRegistry, opts ...registry.Option) error {
// loop through address list and get valid entries
addrs := func(curAddrs []string) []string {
@@ -93,23 +206,20 @@ func configure(g *gossipRegistry, opts ...registry.Option) error {
// shutdown old member
if g.member != nil {
g.member.Shutdown()
g.Stop()
}
// replace addresses
curAddrs = newAddrs
// create a queue
queue := &memberlist.TransmitLimitedQueue{
NumNodes: func() int {
return len(curAddrs)
},
RetransmitMult: 3,
}
// create a new default config
c := memberlist.DefaultLocalConfig()
// sane good default options
c.LogOutput = ioutil.Discard // log to /dev/null
c.PushPullInterval = 0 // disable expensive tcp push/pull
c.ProtocolVersion = 4 // suport latest stable features
if optConfig, ok := g.options.Context.Value(contextConfig{}).(*memberlist.Config); ok && optConfig != nil {
c = optConfig
}
@@ -145,15 +255,6 @@ func configure(g *gossipRegistry, opts ...registry.Option) error {
// set the name
c.Name = strings.Join([]string{"micro", hostname, uuid.New().String()}, "-")
// set the delegate
c.Delegate = &delegate{
updates: g.updates,
queue: queue,
}
// log to dev null
c.LogOutput = ioutil.Discard
// set a secret key if secure
if g.options.Secure {
k, ok := g.options.Context.Value(contextSecretKey{}).([]byte)
@@ -164,52 +265,61 @@ func configure(g *gossipRegistry, opts ...registry.Option) error {
c.SecretKey = k
}
if v, ok := g.options.Context.Value(connectRetry{}).(bool); ok && v {
g.connectRetry = true
}
if td, ok := g.options.Context.Value(connectTimeout{}).(time.Duration); ok {
g.connectTimeout = td
}
// create a queue
queue := &memberlist.TransmitLimitedQueue{
NumNodes: func() int {
return len(curAddrs)
},
RetransmitMult: 3,
}
// set the delegate
c.Delegate = &delegate{
updates: g.updates,
queue: queue,
}
if g.connectRetry {
c.Events = &eventDelegate{
events: g.events,
}
}
// create the memberlist
m, err := memberlist.Create(c)
if err != nil {
return err
}
// join the memberlist
// set internals
g.Lock()
if len(curAddrs) > 0 {
_, err := m.Join(curAddrs)
if err != nil {
return err
for _, addr := range curAddrs {
g.members[addr] = nodeActionUnknown
}
}
// set internals
g.tcpInterval = c.PushPullInterval
g.addrs = curAddrs
g.queue = queue
g.member = m
g.interval = c.GossipInterval
g.Unlock()
log.Logf("Registry Listening on %s", m.LocalNode().Address())
return nil
log.Logf("[gossip]: Registry Listening on %s", m.LocalNode().Address())
return g.connect(curAddrs)
}
func (*broadcast) UniqueBroadcast() {}
func (b *broadcast) Invalidates(other memberlist.Broadcast) bool {
up := new(pb.Update)
if err := proto.Unmarshal(other.Message(), up); err != nil {
return false
}
// ids do not match
if b.update.Id == up.Id {
return false
}
// timestamps do not match
if b.update.Timestamp != up.Timestamp {
return false
}
// type does not match
if b.update.Type != up.Type {
return false
}
// invalidates
return true
return false
}
func (b *broadcast) Message() []byte {
@@ -217,6 +327,9 @@ func (b *broadcast) Message() []byte {
if err != nil {
return nil
}
if l := len(up); l > MaxPacketSize {
log.Logf("[gossip]: broadcast message size %d bigger then MaxPacketSize %d", l, MaxPacketSize)
}
return up
}
@@ -242,7 +355,7 @@ func (d *delegate) NotifyMsg(b []byte) {
}
// only process service action
if up.Type != "service" {
if up.Type != updateTypeService {
return
}
@@ -280,7 +393,7 @@ func (d *delegate) LocalState(join bool) []byte {
d.updates <- &update{
Update: &pb.Update{
Action: syncAction,
Action: actionTypeSync,
},
sync: syncCh,
}
@@ -305,11 +418,10 @@ func (d *delegate) MergeRemoteState(buf []byte, join bool) {
if err := json.Unmarshal(buf, &services); err != nil {
return
}
for _, service := range services {
for _, srv := range service {
d.updates <- &update{
Update: &pb.Update{Action: addAction},
Update: &pb.Update{Action: actionTypeCreate},
Service: srv,
sync: nil,
}
@@ -318,7 +430,7 @@ func (d *delegate) MergeRemoteState(buf []byte, join bool) {
}
func (g *gossipRegistry) publish(action string, services []*registry.Service) {
g.s.RLock()
g.RLock()
for _, sub := range g.watchers {
go func(sub chan *registry.Result) {
for _, service := range services {
@@ -326,7 +438,7 @@ func (g *gossipRegistry) publish(action string, services []*registry.Service) {
}
}(sub)
}
g.s.RUnlock()
g.RUnlock()
}
func (g *gossipRegistry) subscribe() (chan *registry.Result, chan bool) {
@@ -335,56 +447,138 @@ func (g *gossipRegistry) subscribe() (chan *registry.Result, chan bool) {
id := uuid.New().String()
g.s.Lock()
g.Lock()
g.watchers[id] = next
g.s.Unlock()
g.Unlock()
go func() {
<-exit
g.s.Lock()
g.Lock()
delete(g.watchers, id)
close(next)
g.s.Unlock()
g.Unlock()
}()
return next, exit
}
func (g *gossipRegistry) wait() {
ctx := g.options.Context
if c, ok := ctx.Value(contextContext{}).(context.Context); ok && c != nil {
ctx = c
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
select {
// wait on kill signal
case <-ch:
// wait on context cancel
case <-ctx.Done():
}
g.Stop()
}
func (g *gossipRegistry) Stop() error {
g.Lock()
if g.done != nil {
close(g.done)
g.done = nil
}
if g.member != nil {
g.member.Leave(g.interval * 2)
g.member.Shutdown()
g.member = nil
}
g.Unlock()
return nil
}
func (g *gossipRegistry) run() {
var mtx sync.Mutex
updates := map[uint64]*update{}
// expiry loop
go func() {
t := time.NewTicker(ExpiryTick)
defer t.Stop()
ticker := time.NewTicker(ExpiryTick)
defer ticker.Stop()
for _ = range t.C {
now := uint64(time.Now().UnixNano())
for {
select {
case <-g.done:
return
case <-ticker.C:
now := uint64(time.Now().UnixNano())
mtx.Lock()
mtx.Lock()
// process all the updates
for k, v := range updates {
// check if expiry time has passed
if d := (v.Update.Timestamp + v.Update.Expires); d < now {
// delete from records
delete(updates, k)
// set to delete
v.Update.Action = delAction
// fire a new update
g.updates <- v
// process all the updates
for k, v := range updates {
// check if expiry time has passed
if d := (v.Update.Expires); d < now {
// delete from records
delete(updates, k)
// set to delete
v.Update.Action = actionTypeDelete
// fire a new update
g.updates <- v
}
}
}
mtx.Unlock()
mtx.Unlock()
}
}
}()
go func() {
for {
select {
case <-g.done:
return
case ed := <-g.events:
// may be not block all registry?
g.Lock()
if _, ok := g.members[ed.node]; ok {
g.members[ed.node] = ed.action
}
g.Unlock()
}
}
}()
if g.connectRetry {
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-g.done:
return
case <-ticker.C:
var addrs []string
g.RLock()
for node, action := range g.members {
if action == nodeActionLeave && g.member.LocalNode().Address() != node {
addrs = append(addrs, node)
}
}
g.RUnlock()
if len(addrs) > 0 {
g.connect(addrs)
}
}
}
}()
}
// process the updates
for u := range g.updates {
switch u.Update.Action {
case addAction:
case actionTypeCreate:
g.Lock()
if service, ok := g.services[u.Service.Name]; !ok {
g.services[u.Service.Name] = []*registry.Service{u.Service}
@@ -395,7 +589,7 @@ func (g *gossipRegistry) run() {
g.Unlock()
// publish update to watchers
go g.publish(addAction, []*registry.Service{u.Service})
go g.publish(actionTypeString(actionTypeCreate), []*registry.Service{u.Service})
// we need to expire the node at some point in the future
if u.Update.Expires > 0 {
@@ -406,7 +600,7 @@ func (g *gossipRegistry) run() {
mtx.Unlock()
}
}
case delAction:
case actionTypeDelete:
g.Lock()
if service, ok := g.services[u.Service.Name]; ok {
if services := delServices(service, []*registry.Service{u.Service}); len(services) == 0 {
@@ -418,7 +612,7 @@ func (g *gossipRegistry) run() {
g.Unlock()
// publish update to watchers
go g.publish(delAction, []*registry.Service{u.Service})
go g.publish(actionTypeString(actionTypeDelete), []*registry.Service{u.Service})
// delete from expiry checks
if hash, err := hashstructure.Hash(u.Service, nil); err == nil {
@@ -426,7 +620,7 @@ func (g *gossipRegistry) run() {
delete(updates, hash)
mtx.Unlock()
}
case syncAction:
case actionTypeSync:
// no sync channel provided
if u.sync == nil {
continue
@@ -441,7 +635,7 @@ func (g *gossipRegistry) run() {
}
// publish to watchers
go g.publish(addAction, service)
go g.publish(actionTypeString(actionTypeCreate), service)
}
g.RUnlock()
@@ -479,12 +673,14 @@ func (g *gossipRegistry) Register(s *registry.Service, opts ...registry.Register
o(&options)
}
if options.TTL == 0 && g.tcpInterval == 0 {
return fmt.Errorf("must provide registry.RegisterTTL option or set PushPullInterval in *memberlist.Config")
}
up := &pb.Update{
Id: uuid.New().String(),
Timestamp: uint64(time.Now().UnixNano()),
Expires: uint64(options.TTL.Nanoseconds()),
Action: "update",
Type: "service",
Expires: uint64(time.Now().Add(options.TTL).UnixNano()),
Action: actionTypeCreate,
Type: updateTypeService,
Metadata: map[string]string{
"Content-Type": "application/json",
},
@@ -519,10 +715,8 @@ func (g *gossipRegistry) Deregister(s *registry.Service) error {
g.Unlock()
up := &pb.Update{
Id: uuid.New().String(),
Timestamp: uint64(time.Now().UnixNano()),
Action: "delete",
Type: "service",
Action: actionTypeDelete,
Type: updateTypeService,
Metadata: map[string]string{
"Content-Type": "application/json",
},
@@ -575,8 +769,11 @@ func NewRegistry(opts ...registry.Option) registry.Registry {
Context: context.Background(),
},
updates: make(chan *update, 100),
events: make(chan *event, 100),
services: make(map[string][]*registry.Service),
watchers: make(map[string]chan *registry.Result),
done: make(chan struct{}),
members: make(map[string]int32),
}
// run the updater
@@ -590,5 +787,7 @@ func NewRegistry(opts ...registry.Option) registry.Registry {
// wait for setup
<-time.After(gossip.interval * 2)
go gossip.wait()
return gossip
}

View File

@@ -0,0 +1,223 @@
package gossip
import (
"os"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/hashicorp/memberlist"
"github.com/micro/go-micro/registry"
)
func newMemberlistConfig() *memberlist.Config {
mc := memberlist.DefaultLANConfig()
mc.DisableTcpPings = false
mc.GossipVerifyIncoming = false
mc.GossipVerifyOutgoing = false
mc.EnableCompression = false
mc.PushPullInterval = 3 * time.Second
mc.LogOutput = os.Stderr
mc.ProtocolVersion = 4
mc.Name = uuid.New().String()
return mc
}
func newRegistry(opts ...registry.Option) registry.Registry {
options := []registry.Option{
ConnectRetry(true),
ConnectTimeout(60 * time.Second),
}
options = append(options, opts...)
r := NewRegistry(options...)
return r
}
func TestRegistryBroadcast(t *testing.T) {
mc1 := newMemberlistConfig()
r1 := newRegistry(Config(mc1), Address("127.0.0.1:54321"))
mc2 := newMemberlistConfig()
r2 := newRegistry(Config(mc2), Address("127.0.0.2:54321"), registry.Addrs("127.0.0.1:54321"))
defer r1.(*gossipRegistry).Stop()
defer r2.(*gossipRegistry).Stop()
svc1 := &registry.Service{Name: "r1-svc", Version: "0.0.0.1"}
svc2 := &registry.Service{Name: "r2-svc", Version: "0.0.0.2"}
t.Logf("register service svc1 on r1\n")
if err := r1.Register(svc1, registry.RegisterTTL(10*time.Second)); err != nil {
t.Fatal(err)
}
t.Logf("register service svc2 on r2\n")
if err := r2.Register(svc2, registry.RegisterTTL(10*time.Second)); err != nil {
t.Fatal(err)
}
var found bool
t.Logf("list services on r1\n")
svcs, err := r1.ListServices()
if err != nil {
t.Fatal(err)
}
for _, svc := range svcs {
if svc.Name == "r2-svc" {
found = true
}
}
if !found {
t.Fatalf("r2-svc not found in r1, broadcast not work")
} else {
t.Logf("r2-svc found in r1, all ok")
}
found = false
t.Logf("list services on r2\n")
svcs, err = r2.ListServices()
if err != nil {
t.Fatal(err)
}
for _, svc := range svcs {
if svc.Name == "r1-svc" {
found = true
}
}
if !found {
t.Fatalf("r1-svc not found in r2, broadcast not work")
} else {
t.Logf("r1-svc found in r1, all ok")
}
t.Logf("deregister service svc1 on r1\n")
if err := r1.Deregister(svc1); err != nil {
t.Fatal(err)
}
t.Logf("deregister service svc1 on r2\n")
if err := r2.Deregister(svc2); err != nil {
t.Fatal(err)
}
}
func TestRegistryRetry(t *testing.T) {
mc1 := newMemberlistConfig()
r1 := newRegistry(Config(mc1), Address("127.0.0.1:54321"))
mc2 := newMemberlistConfig()
r2 := newRegistry(Config(mc2), Address("127.0.0.2:54321"), registry.Addrs("127.0.0.1:54321"))
defer r1.(*gossipRegistry).Stop()
defer r2.(*gossipRegistry).Stop()
svc1 := &registry.Service{Name: "r1-svc", Version: "0.0.0.1"}
svc2 := &registry.Service{Name: "r2-svc", Version: "0.0.0.2"}
var mu sync.Mutex
ch := make(chan struct{})
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
go func() {
for {
select {
case <-ticker.C:
mu.Lock()
if r1 != nil {
r1.Register(svc1, registry.RegisterTTL(2*time.Second))
}
if r2 != nil {
r2.Register(svc2, registry.RegisterTTL(2*time.Second))
}
if ch != nil {
close(ch)
ch = nil
}
mu.Unlock()
}
}
}()
<-ch
var found bool
svcs, err := r2.ListServices()
if err != nil {
t.Fatal(err)
}
for _, svc := range svcs {
if svc.Name == "r1-svc" {
found = true
}
}
if !found {
t.Fatalf("r1-svc not found in r2, broadcast not work, retry cant test")
}
t.Logf("stop r1\n")
if err = r1.(*gossipRegistry).Stop(); err != nil {
t.Fatalf("cant stop r1 registry %v", err)
}
mu.Lock()
r1 = nil
mu.Unlock()
<-time.After(3 * time.Second)
found = false
svcs, err = r2.ListServices()
if err != nil {
t.Fatal(err)
}
for _, svc := range svcs {
if svc.Name == "r1-svc" {
found = true
}
}
if found {
t.Fatalf("r1-svc found in r2, something wrong")
}
t.Logf("start r1\n")
r1 = newRegistry(Config(mc1), Address("127.0.0.1:54321"))
<-time.After(2 * time.Second)
if tr := os.Getenv("TRAVIS"); len(tr) > 0 {
t.Logf("skip next test part, becasue it not works in travis")
t.Skip()
return
<-time.After(5 * time.Second)
}
found = false
svcs, err = r2.ListServices()
if err != nil {
t.Fatal(err)
}
for _, svc := range svcs {
if svc.Name == "r1-svc" {
found = true
}
}
if !found {
t.Fatalf("r1-svc not found in r2, connect retry not works")
}
if err := r1.Deregister(svc1); err != nil {
t.Fatal(err)
}
if err := r2.Deregister(svc2); err != nil {
t.Fatal(err)
}
r1.(*gossipRegistry).Stop()
r2.(*gossipRegistry).Stop()
}

View File

@@ -2,6 +2,7 @@ package gossip
import (
"context"
"time"
"github.com/hashicorp/memberlist"
"github.com/micro/go-micro/registry"
@@ -12,34 +13,50 @@ type contextSecretKey struct{}
// Secret specifies an encryption key. The value should be either
// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.
func Secret(k []byte) registry.Option {
return func(o *registry.Options) {
o.Context = context.WithValue(o.Context, contextSecretKey{}, k)
}
return setRegistryOption(contextSecretKey{}, k)
}
type contextAddress struct{}
// Address to bind to - host:port
func Address(a string) registry.Option {
return func(o *registry.Options) {
o.Context = context.WithValue(o.Context, contextAddress{}, a)
}
return setRegistryOption(contextAddress{}, a)
}
type contextConfig struct{}
// Config allow to inject a *memberlist.Config struct for configuring gossip
func Config(c *memberlist.Config) registry.Option {
return func(o *registry.Options) {
o.Context = context.WithValue(o.Context, contextConfig{}, c)
}
return setRegistryOption(contextConfig{}, c)
}
type contextAdvertise struct{}
// The address to advertise for other gossip members - host:port
func Advertise(a string) registry.Option {
return func(o *registry.Options) {
o.Context = context.WithValue(o.Context, contextAdvertise{}, a)
}
return setRegistryOption(contextAdvertise{}, a)
}
type contextContext struct{}
// Context specifies a context for the registry.
// Can be used to signal shutdown of the registry.
// Can be used for extra option values.
func Context(ctx context.Context) registry.Option {
return setRegistryOption(contextContext{}, ctx)
}
type connectTimeout struct{}
// ConnectTimeout specify registry connect timeout use -1 to specify infinite
func ConnectTimeout(td time.Duration) registry.Option {
return setRegistryOption(connectTimeout{}, td)
}
type connectRetry struct{}
// ConnectRetry enable reconnect to registry then connection closed,
// use with ConnectTimeout to specify how long retry
func ConnectRetry(v bool) registry.Option {
return setRegistryOption(connectRetry{}, v)
}

View File

@@ -5,8 +5,9 @@ package gossip
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
proto "github.com/golang/protobuf/proto"
)
// Reference imports to suppress errors if they are not otherwise used.
@@ -18,20 +19,16 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// Update is the message broadcast
type Update struct {
// unique id of update
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// unix nano timestamp of update
Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// time to live for entry
Expires uint64 `protobuf:"varint,3,opt,name=expires,proto3" json:"expires,omitempty"`
// type of update; service
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
// what action is taken; add, del, put
Action string `protobuf:"bytes,5,opt,name=action,proto3" json:"action,omitempty"`
Expires uint64 `protobuf:"varint,1,opt,name=expires,proto3" json:"expires,omitempty"`
// type of update
Type int32 `protobuf:"varint,2,opt,name=type,proto3" json:"type,omitempty"`
// what action is taken
Action int32 `protobuf:"varint,3,opt,name=action,proto3" json:"action,omitempty"`
// any other associated metadata about the data
Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
// the payload data;
@@ -66,20 +63,6 @@ func (m *Update) XXX_DiscardUnknown() {
var xxx_messageInfo_Update proto.InternalMessageInfo
func (m *Update) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Update) GetTimestamp() uint64 {
if m != nil {
return m.Timestamp
}
return 0
}
func (m *Update) GetExpires() uint64 {
if m != nil {
return m.Expires
@@ -87,18 +70,18 @@ func (m *Update) GetExpires() uint64 {
return 0
}
func (m *Update) GetType() string {
func (m *Update) GetType() int32 {
if m != nil {
return m.Type
}
return ""
return 0
}
func (m *Update) GetAction() string {
func (m *Update) GetAction() int32 {
if m != nil {
return m.Action
}
return ""
return 0
}
func (m *Update) GetMetadata() map[string]string {
@@ -125,21 +108,20 @@ func init() {
}
var fileDescriptor_18cba623e76e57f3 = []byte{
// 251 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0xcf, 0x4a, 0xc4, 0x30,
0x10, 0x87, 0x69, 0xb6, 0x9b, 0xb5, 0xe3, 0x1f, 0x64, 0x10, 0x09, 0xb2, 0x87, 0xe2, 0xa9, 0x17,
0x5b, 0xd0, 0xcb, 0xa2, 0x5e, 0x3d, 0x7a, 0x09, 0xf8, 0x00, 0xd9, 0x36, 0xd4, 0xa0, 0xd9, 0x84,
0x64, 0x56, 0xec, 0x13, 0xf8, 0xda, 0xb2, 0x69, 0x54, 0xbc, 0x7d, 0xdf, 0xcc, 0x24, 0x99, 0x5f,
0xe0, 0x71, 0x34, 0xf4, 0xba, 0xdf, 0xb6, 0xbd, 0xb3, 0x9d, 0x35, 0x7d, 0x70, 0xdd, 0xe8, 0x6e,
0x66, 0x08, 0x7a, 0x34, 0x91, 0xc2, 0xd4, 0x8d, 0x2e, 0x46, 0xe3, 0x3b, 0x1f, 0x1c, 0xb9, 0x2c,
0x6d, 0x12, 0xe4, 0xb3, 0x5d, 0x7f, 0x31, 0xe0, 0x2f, 0x7e, 0x50, 0xa4, 0xf1, 0x0c, 0x98, 0x19,
0x44, 0x51, 0x17, 0x4d, 0x25, 0x99, 0x19, 0x70, 0x0d, 0x15, 0x19, 0xab, 0x23, 0x29, 0xeb, 0x05,
0xab, 0x8b, 0xa6, 0x94, 0x7f, 0x05, 0x14, 0xb0, 0xd2, 0x9f, 0xde, 0x04, 0x1d, 0xc5, 0x22, 0xf5,
0x7e, 0x14, 0x11, 0x4a, 0x9a, 0xbc, 0x16, 0x65, 0xba, 0x29, 0x31, 0x5e, 0x02, 0x57, 0x3d, 0x19,
0xb7, 0x13, 0xcb, 0x54, 0xcd, 0x86, 0x1b, 0x38, 0xb2, 0x9a, 0xd4, 0xa0, 0x48, 0x09, 0x5e, 0x2f,
0x9a, 0xe3, 0xdb, 0x75, 0x9b, 0xf7, 0x9c, 0xb7, 0x6a, 0x9f, 0x73, 0xfb, 0x69, 0x47, 0x61, 0x92,
0xbf, 0xd3, 0x87, 0x57, 0xd2, 0xa9, 0x55, 0x5d, 0x34, 0x27, 0x32, 0xf1, 0xd5, 0x03, 0x9c, 0xfe,
0x1b, 0xc7, 0x73, 0x58, 0xbc, 0xe9, 0x29, 0x67, 0x3a, 0x20, 0x5e, 0xc0, 0xf2, 0x43, 0xbd, 0xef,
0x75, 0x0a, 0x54, 0xc9, 0x59, 0xee, 0xd9, 0xa6, 0xd8, 0xf2, 0xf4, 0x31, 0x77, 0xdf, 0x01, 0x00,
0x00, 0xff, 0xff, 0x06, 0x6e, 0x00, 0x3c, 0x58, 0x01, 0x00, 0x00,
// 227 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0xc1, 0x4a, 0x03, 0x31,
0x14, 0x45, 0x49, 0xa7, 0x4d, 0xed, 0x53, 0x41, 0x1e, 0x22, 0x41, 0x5c, 0x0c, 0xae, 0x66, 0xe3,
0x0c, 0xe8, 0xa6, 0xa8, 0x5b, 0x97, 0x6e, 0x02, 0x7e, 0x40, 0x3a, 0x0d, 0x31, 0xe8, 0x34, 0x21,
0x79, 0x15, 0xf3, 0xa9, 0xfe, 0x8d, 0x34, 0x89, 0x42, 0x77, 0xe7, 0x24, 0x37, 0xdc, 0x1b, 0x78,
0x36, 0x96, 0xde, 0xf7, 0x9b, 0x7e, 0x74, 0xd3, 0x30, 0xd9, 0x31, 0xb8, 0xc1, 0xb8, 0xbb, 0x02,
0x41, 0x1b, 0x1b, 0x29, 0xa4, 0xc1, 0xb8, 0x18, 0xad, 0x1f, 0x7c, 0x70, 0xe4, 0xaa, 0xf4, 0x59,
0x90, 0x17, 0xbb, 0xfd, 0x61, 0xc0, 0xdf, 0xfc, 0x56, 0x91, 0x46, 0x01, 0x4b, 0xfd, 0xed, 0x6d,
0xd0, 0x51, 0xb0, 0x96, 0x75, 0x73, 0xf9, 0xa7, 0x88, 0x30, 0xa7, 0xe4, 0xb5, 0x98, 0xb5, 0xac,
0x5b, 0xc8, 0xcc, 0x78, 0x05, 0x5c, 0x8d, 0x64, 0xdd, 0x4e, 0x34, 0xf9, 0xb4, 0x1a, 0xae, 0xe1,
0x64, 0xd2, 0xa4, 0xb6, 0x8a, 0x94, 0xe0, 0x6d, 0xd3, 0x9d, 0xde, 0xdf, 0xf4, 0xb5, 0xb9, 0xf4,
0xf4, 0xaf, 0xf5, 0xfa, 0x65, 0x47, 0x21, 0xc9, 0xff, 0xf4, 0xa1, 0x25, 0xbf, 0x5a, 0xb6, 0xac,
0x3b, 0x93, 0x99, 0xaf, 0x9f, 0xe0, 0xfc, 0x28, 0x8e, 0x17, 0xd0, 0x7c, 0xe8, 0x94, 0x07, 0xae,
0xe4, 0x01, 0xf1, 0x12, 0x16, 0x5f, 0xea, 0x73, 0x5f, 0xd6, 0xad, 0x64, 0x91, 0xc7, 0xd9, 0x9a,
0x6d, 0x78, 0xfe, 0xea, 0xc3, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd6, 0x63, 0x7b, 0x1b, 0x2a,
0x01, 0x00, 0x00,
}

View File

@@ -4,16 +4,12 @@ package gossip;
// Update is the message broadcast
message Update {
// unique id of update
string id = 1;
// unix nano timestamp of update
uint64 timestamp = 2;
// time to live for entry
uint64 expires = 3;
// type of update; service
string type = 4;
// what action is taken; add, del, put
string action = 5;
uint64 expires = 1;
// type of update
int32 type = 2;
// what action is taken
int32 action = 3;
// any other associated metadata about the data
map<string, string> metadata = 6;
// the payload data;

View File

@@ -70,21 +70,30 @@ func addNodes(old, neu []*registry.Node) []*registry.Node {
}
func addServices(old, neu []*registry.Service) []*registry.Service {
var srv []*registry.Service
for _, s := range neu {
var seen bool
for i, o := range old {
for _, o := range old {
if o.Version == s.Version {
s.Nodes = addNodes(o.Nodes, s.Nodes)
sp := new(registry.Service)
// make copy
*sp = *o
// set nodes
sp.Nodes = addNodes(o.Nodes, s.Nodes)
// mark as seen
seen = true
old[i] = s
srv = append(srv, sp)
break
}
}
if !seen {
old = append(old, s)
srv = append(srv, cp([]*registry.Service{s})...)
}
}
return old
return srv
}
func delNodes(old, del []*registry.Node) []*registry.Node {

View File

@@ -1,73 +0,0 @@
package mdns
import (
"bytes"
"compress/zlib"
"encoding/hex"
"encoding/json"
"io/ioutil"
"strings"
)
func encode(txt *mdnsTxt) ([]string, error) {
b, err := json.Marshal(txt)
if err != nil {
return nil, err
}
var buf bytes.Buffer
defer buf.Reset()
w := zlib.NewWriter(&buf)
if _, err := w.Write(b); err != nil {
return nil, err
}
w.Close()
encoded := hex.EncodeToString(buf.Bytes())
// individual txt limit
if len(encoded) <= 255 {
return []string{encoded}, nil
}
// split encoded string
var record []string
for len(encoded) > 255 {
record = append(record, encoded[:255])
encoded = encoded[255:]
}
record = append(record, encoded)
return record, nil
}
func decode(record []string) (*mdnsTxt, error) {
encoded := strings.Join(record, "")
hr, err := hex.DecodeString(encoded)
if err != nil {
return nil, err
}
br := bytes.NewReader(hr)
zr, err := zlib.NewReader(br)
if err != nil {
return nil, err
}
rbuf, err := ioutil.ReadAll(zr)
if err != nil {
return nil, err
}
var txt *mdnsTxt
if err := json.Unmarshal(rbuf, &txt); err != nil {
return nil, err
}
return txt, nil
}

View File

@@ -1,67 +0,0 @@
package mdns
import (
"testing"
"github.com/micro/go-micro/registry"
)
func TestEncoding(t *testing.T) {
testData := []*mdnsTxt{
&mdnsTxt{
Version: "1.0.0",
Metadata: map[string]string{
"foo": "bar",
},
Endpoints: []*registry.Endpoint{
&registry.Endpoint{
Name: "endpoint1",
Request: &registry.Value{
Name: "request",
Type: "request",
},
Response: &registry.Value{
Name: "response",
Type: "response",
},
Metadata: map[string]string{
"foo1": "bar1",
},
},
},
},
}
for _, d := range testData {
encoded, err := encode(d)
if err != nil {
t.Fatal(err)
}
for _, txt := range encoded {
if len(txt) > 255 {
t.Fatalf("One of parts for txt is %d characters", len(txt))
}
}
decoded, err := decode(encoded)
if err != nil {
t.Fatal(err)
}
if decoded.Version != d.Version {
t.Fatalf("Expected version %s got %s", d.Version, decoded.Version)
}
if len(decoded.Endpoints) != len(d.Endpoints) {
t.Fatalf("Expected %d endpoints, got %d", len(d.Endpoints), len(decoded.Endpoints))
}
for k, v := range d.Metadata {
if val := decoded.Metadata[k]; val != v {
t.Fatalf("Expected %s=%s got %s=%s", k, v, k, val)
}
}
}
}

View File

@@ -1,339 +1,11 @@
// Package mdns is a multicast dns registry
// Package mdns provides a multicast dns registry
package mdns
/*
MDNS is a multicast dns registry for service discovery
This creates a zero dependency system which is great
where multicast dns is available. This usually depends
on the ability to leverage udp and multicast/broadcast.
*/
import (
"net"
"strings"
"sync"
"time"
"github.com/micro/go-micro/registry"
"github.com/micro/mdns"
hash "github.com/mitchellh/hashstructure"
)
type mdnsTxt struct {
Service string
Version string
Endpoints []*registry.Endpoint
Metadata map[string]string
}
type mdnsEntry struct {
hash uint64
id string
node *mdns.Server
}
type mdnsRegistry struct {
opts registry.Options
sync.Mutex
services map[string][]*mdnsEntry
}
func newRegistry(opts ...registry.Option) registry.Registry {
options := registry.Options{
Timeout: time.Millisecond * 100,
}
return &mdnsRegistry{
opts: options,
services: make(map[string][]*mdnsEntry),
}
}
func (m *mdnsRegistry) Init(opts ...registry.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *mdnsRegistry) Options() registry.Options {
return m.opts
}
func (m *mdnsRegistry) Register(service *registry.Service, opts ...registry.RegisterOption) error {
m.Lock()
defer m.Unlock()
entries, ok := m.services[service.Name]
// first entry, create wildcard used for list queries
if !ok {
s, err := mdns.NewMDNSService(
service.Name,
"_services",
"",
"",
9999,
[]net.IP{net.ParseIP("0.0.0.0")},
nil,
)
if err != nil {
return err
}
srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{s}})
if err != nil {
return err
}
// append the wildcard entry
entries = append(entries, &mdnsEntry{id: "*", node: srv})
}
var gerr error
for _, node := range service.Nodes {
// create hash of service; uint64
h, err := hash.Hash(node, nil)
if err != nil {
gerr = err
continue
}
var seen bool
var e *mdnsEntry
for _, entry := range entries {
if node.Id == entry.id {
seen = true
e = entry
break
}
}
// already registered, continue
if seen && e.hash == h {
continue
// hash doesn't match, shutdown
} else if seen {
e.node.Shutdown()
// doesn't exist
} else {
e = &mdnsEntry{hash: h}
}
txt, err := encode(&mdnsTxt{
Service: service.Name,
Version: service.Version,
Endpoints: service.Endpoints,
Metadata: node.Metadata,
})
if err != nil {
gerr = err
continue
}
// we got here, new node
s, err := mdns.NewMDNSService(
node.Id,
service.Name,
"",
"",
node.Port,
[]net.IP{net.ParseIP(node.Address)},
txt,
)
if err != nil {
gerr = err
continue
}
srv, err := mdns.NewServer(&mdns.Config{Zone: s})
if err != nil {
gerr = err
continue
}
e.id = node.Id
e.node = srv
entries = append(entries, e)
}
// save
m.services[service.Name] = entries
return gerr
}
func (m *mdnsRegistry) Deregister(service *registry.Service) error {
m.Lock()
defer m.Unlock()
var newEntries []*mdnsEntry
// loop existing entries, check if any match, shutdown those that do
for _, entry := range m.services[service.Name] {
var remove bool
for _, node := range service.Nodes {
if node.Id == entry.id {
entry.node.Shutdown()
remove = true
break
}
}
// keep it?
if !remove {
newEntries = append(newEntries, entry)
}
}
// last entry is the wildcard for list queries. Remove it.
if len(newEntries) == 1 && newEntries[0].id == "*" {
newEntries[0].node.Shutdown()
delete(m.services, service.Name)
} else {
m.services[service.Name] = newEntries
}
return nil
}
func (m *mdnsRegistry) GetService(service string) ([]*registry.Service, error) {
p := mdns.DefaultParams(service)
p.Timeout = m.opts.Timeout
entryCh := make(chan *mdns.ServiceEntry, 10)
p.Entries = entryCh
exit := make(chan bool)
defer close(exit)
serviceMap := make(map[string]*registry.Service)
go func() {
for {
select {
case e := <-entryCh:
// list record so skip
if p.Service == "_services" {
continue
}
if e.TTL == 0 {
continue
}
txt, err := decode(e.InfoFields)
if err != nil {
continue
}
if txt.Service != service {
continue
}
s, ok := serviceMap[txt.Version]
if !ok {
s = &registry.Service{
Name: txt.Service,
Version: txt.Version,
Endpoints: txt.Endpoints,
}
}
s.Nodes = append(s.Nodes, &registry.Node{
Id: strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+"."),
Address: e.AddrV4.String(),
Port: e.Port,
Metadata: txt.Metadata,
})
serviceMap[txt.Version] = s
case <-exit:
return
}
}
}()
if err := mdns.Query(p); err != nil {
return nil, err
}
// create list and return
var services []*registry.Service
for _, service := range serviceMap {
services = append(services, service)
}
return services, nil
}
func (m *mdnsRegistry) ListServices() ([]*registry.Service, error) {
p := mdns.DefaultParams("_services")
p.Timeout = m.opts.Timeout
entryCh := make(chan *mdns.ServiceEntry, 10)
p.Entries = entryCh
exit := make(chan bool)
defer close(exit)
serviceMap := make(map[string]bool)
var services []*registry.Service
go func() {
for {
select {
case e := <-entryCh:
if e.TTL == 0 {
continue
}
name := strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+".")
if !serviceMap[name] {
serviceMap[name] = true
services = append(services, &registry.Service{Name: name})
}
case <-exit:
return
}
}
}()
if err := mdns.Query(p); err != nil {
return nil, err
}
return services, nil
}
func (m *mdnsRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
var wo registry.WatchOptions
for _, o := range opts {
o(&wo)
}
md := &mdnsWatcher{
wo: wo,
ch: make(chan *mdns.ServiceEntry, 32),
exit: make(chan struct{}),
}
go func() {
if err := mdns.Listen(md.ch, md.exit); err != nil {
md.Stop()
}
}()
return md, nil
}
func (m *mdnsRegistry) String() string {
return "mdns"
}
// NewRegistry returns a new mdns registry
func NewRegistry(opts ...registry.Option) registry.Registry {
return newRegistry(opts...)
return registry.NewRegistry(opts...)
}

344
registry/mdns_registry.go Normal file
View File

@@ -0,0 +1,344 @@
// Package mdns is a multicast dns registry
package registry
import (
"context"
"net"
"strings"
"sync"
"time"
"github.com/micro/mdns"
hash "github.com/mitchellh/hashstructure"
)
type mdnsTxt struct {
Service string
Version string
Endpoints []*Endpoint
Metadata map[string]string
}
type mdnsEntry struct {
hash uint64
id string
node *mdns.Server
}
type mdnsRegistry struct {
opts Options
sync.Mutex
services map[string][]*mdnsEntry
}
func newRegistry(opts ...Option) Registry {
options := Options{
Timeout: time.Millisecond * 100,
}
return &mdnsRegistry{
opts: options,
services: make(map[string][]*mdnsEntry),
}
}
func (m *mdnsRegistry) Init(opts ...Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *mdnsRegistry) Options() Options {
return m.opts
}
func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error {
m.Lock()
defer m.Unlock()
entries, ok := m.services[service.Name]
// first entry, create wildcard used for list queries
if !ok {
s, err := mdns.NewMDNSService(
service.Name,
"_services",
"",
"",
9999,
[]net.IP{net.ParseIP("0.0.0.0")},
nil,
)
if err != nil {
return err
}
srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{s}})
if err != nil {
return err
}
// append the wildcard entry
entries = append(entries, &mdnsEntry{id: "*", node: srv})
}
var gerr error
for _, node := range service.Nodes {
// create hash of service; uint64
h, err := hash.Hash(node, nil)
if err != nil {
gerr = err
continue
}
var seen bool
var e *mdnsEntry
for _, entry := range entries {
if node.Id == entry.id {
seen = true
e = entry
break
}
}
// already registered, continue
if seen && e.hash == h {
continue
// hash doesn't match, shutdown
} else if seen {
e.node.Shutdown()
// doesn't exist
} else {
e = &mdnsEntry{hash: h}
}
txt, err := encode(&mdnsTxt{
Service: service.Name,
Version: service.Version,
Endpoints: service.Endpoints,
Metadata: node.Metadata,
})
if err != nil {
gerr = err
continue
}
// we got here, new node
s, err := mdns.NewMDNSService(
node.Id,
service.Name,
"",
"",
node.Port,
[]net.IP{net.ParseIP(node.Address)},
txt,
)
if err != nil {
gerr = err
continue
}
srv, err := mdns.NewServer(&mdns.Config{Zone: s})
if err != nil {
gerr = err
continue
}
e.id = node.Id
e.node = srv
entries = append(entries, e)
}
// save
m.services[service.Name] = entries
return gerr
}
func (m *mdnsRegistry) Deregister(service *Service) error {
m.Lock()
defer m.Unlock()
var newEntries []*mdnsEntry
// loop existing entries, check if any match, shutdown those that do
for _, entry := range m.services[service.Name] {
var remove bool
for _, node := range service.Nodes {
if node.Id == entry.id {
entry.node.Shutdown()
remove = true
break
}
}
// keep it?
if !remove {
newEntries = append(newEntries, entry)
}
}
// last entry is the wildcard for list queries. Remove it.
if len(newEntries) == 1 && newEntries[0].id == "*" {
newEntries[0].node.Shutdown()
delete(m.services, service.Name)
} else {
m.services[service.Name] = newEntries
}
return nil
}
func (m *mdnsRegistry) GetService(service string) ([]*Service, error) {
serviceMap := make(map[string]*Service)
entries := make(chan *mdns.ServiceEntry, 10)
done := make(chan bool)
p := mdns.DefaultParams(service)
// set context with timeout
p.Context, _ = context.WithTimeout(context.Background(), m.opts.Timeout)
// set entries channel
p.Entries = entries
go func() {
for {
select {
case e := <-entries:
// list record so skip
if p.Service == "_services" {
continue
}
if e.TTL == 0 {
continue
}
txt, err := decode(e.InfoFields)
if err != nil {
continue
}
if txt.Service != service {
continue
}
s, ok := serviceMap[txt.Version]
if !ok {
s = &Service{
Name: txt.Service,
Version: txt.Version,
Endpoints: txt.Endpoints,
}
}
s.Nodes = append(s.Nodes, &Node{
Id: strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+"."),
Address: e.AddrV4.String(),
Port: e.Port,
Metadata: txt.Metadata,
})
serviceMap[txt.Version] = s
case <-p.Context.Done():
close(done)
return
}
}
}()
// execute the query
if err := mdns.Query(p); err != nil {
return nil, err
}
// wait for completion
<-done
// create list and return
var services []*Service
for _, service := range serviceMap {
services = append(services, service)
}
return services, nil
}
func (m *mdnsRegistry) ListServices() ([]*Service, error) {
serviceMap := make(map[string]bool)
entries := make(chan *mdns.ServiceEntry, 10)
done := make(chan bool)
p := mdns.DefaultParams("_services")
// set context with timeout
p.Context, _ = context.WithTimeout(context.Background(), m.opts.Timeout)
// set entries channel
p.Entries = entries
var services []*Service
go func() {
for {
select {
case e := <-entries:
if e.TTL == 0 {
continue
}
name := strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+".")
if !serviceMap[name] {
serviceMap[name] = true
services = append(services, &Service{Name: name})
}
case <-p.Context.Done():
close(done)
return
}
}
}()
// execute query
if err := mdns.Query(p); err != nil {
return nil, err
}
// wait till done
<-done
return services, nil
}
func (m *mdnsRegistry) Watch(opts ...WatchOption) (Watcher, error) {
var wo WatchOptions
for _, o := range opts {
o(&wo)
}
md := &mdnsWatcher{
wo: wo,
ch: make(chan *mdns.ServiceEntry, 32),
exit: make(chan struct{}),
}
go func() {
if err := mdns.Listen(md.ch, md.exit); err != nil {
md.Stop()
}
}()
return md, nil
}
func (m *mdnsRegistry) String() string {
return "mdns"
}
// NewRegistry returns a new default registry which is mdns
func NewRegistry(opts ...Option) Registry {
return newRegistry(opts...)
}

View File

@@ -1,19 +1,17 @@
package mdns
package registry
import (
"testing"
"time"
"github.com/micro/go-micro/registry"
)
func TestMDNS(t *testing.T) {
testData := []*registry.Service{
&registry.Service{
testData := []*Service{
&Service{
Name: "test1",
Version: "1.0.1",
Nodes: []*registry.Node{
&registry.Node{
Nodes: []*Node{
&Node{
Id: "test1-1",
Address: "10.0.0.1",
Port: 10001,
@@ -23,11 +21,11 @@ func TestMDNS(t *testing.T) {
},
},
},
&registry.Service{
&Service{
Name: "test2",
Version: "1.0.2",
Nodes: []*registry.Node{
&registry.Node{
Nodes: []*Node{
&Node{
Id: "test2-1",
Address: "10.0.0.2",
Port: 10002,
@@ -37,11 +35,11 @@ func TestMDNS(t *testing.T) {
},
},
},
&registry.Service{
&Service{
Name: "test3",
Version: "1.0.3",
Nodes: []*registry.Node{
&registry.Node{
Nodes: []*Node{
&Node{
Id: "test3-1",
Address: "10.0.0.3",
Port: 10003,

View File

@@ -1,20 +1,19 @@
package mdns
package registry
import (
"errors"
"strings"
"github.com/micro/go-micro/registry"
"github.com/micro/mdns"
)
type mdnsWatcher struct {
wo registry.WatchOptions
wo WatchOptions
ch chan *mdns.ServiceEntry
exit chan struct{}
}
func (m *mdnsWatcher) Next() (*registry.Result, error) {
func (m *mdnsWatcher) Next() (*Result, error) {
for {
select {
case e := <-m.ch:
@@ -41,7 +40,7 @@ func (m *mdnsWatcher) Next() (*registry.Result, error) {
action = "create"
}
service := &registry.Service{
service := &Service{
Name: txt.Service,
Version: txt.Version,
Endpoints: txt.Endpoints,
@@ -52,14 +51,14 @@ func (m *mdnsWatcher) Next() (*registry.Result, error) {
continue
}
service.Nodes = append(service.Nodes, &registry.Node{
service.Nodes = append(service.Nodes, &Node{
Id: strings.TrimSuffix(e.Name, "."+service.Name+".local."),
Address: e.AddrV4.String(),
Port: e.Port,
Metadata: txt.Metadata,
})
return &registry.Result{
return &Result{
Action: action,
Service: service,
}, nil

51
registry/memory/data.go Normal file
View File

@@ -0,0 +1,51 @@
package memory
import (
"github.com/micro/go-micro/registry"
)
var (
// mock data
Data = map[string][]*registry.Service{
"foo": []*registry.Service{
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-1.0.0-123",
Address: "localhost",
Port: 9999,
},
{
Id: "foo-1.0.0-321",
Address: "localhost",
Port: 9999,
},
},
},
{
Name: "foo",
Version: "1.0.1",
Nodes: []*registry.Node{
{
Id: "foo-1.0.1-321",
Address: "localhost",
Port: 6666,
},
},
},
{
Name: "foo",
Version: "1.0.3",
Nodes: []*registry.Node{
{
Id: "foo-1.0.3-345",
Address: "localhost",
Port: 8888,
},
},
},
},
}
)

View File

@@ -1,4 +1,4 @@
package mock
package memory
import (
"github.com/micro/go-micro/registry"

View File

@@ -1,4 +1,4 @@
package mock
package memory
import (
"testing"

160
registry/memory/memory.go Normal file
View File

@@ -0,0 +1,160 @@
// Package memory provides an in-memory registry
package memory
import (
"context"
"sync"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/registry"
)
type Registry struct {
options registry.Options
sync.RWMutex
Services map[string][]*registry.Service
Watchers map[string]*Watcher
}
var (
timeout = time.Millisecond * 10
)
// Setup sets mock data
func (m *Registry) Setup() {
m.Lock()
defer m.Unlock()
// add some memory data
m.Services = Data
}
func (m *Registry) watch(r *registry.Result) {
var watchers []*Watcher
m.RLock()
for _, w := range m.Watchers {
watchers = append(watchers, w)
}
m.RUnlock()
for _, w := range watchers {
select {
case <-w.exit:
m.Lock()
delete(m.Watchers, w.id)
m.Unlock()
default:
select {
case w.res <- r:
case <-time.After(timeout):
}
}
}
}
func (m *Registry) Init(opts ...registry.Option) error {
for _, o := range opts {
o(&m.options)
}
// add services
m.Lock()
for k, v := range getServices(m.options.Context) {
s := m.Services[k]
m.Services[k] = addServices(s, v)
}
m.Unlock()
return nil
}
func (m *Registry) Options() registry.Options {
return m.options
}
func (m *Registry) GetService(service string) ([]*registry.Service, error) {
m.RLock()
s, ok := m.Services[service]
if !ok || len(s) == 0 {
m.RUnlock()
return nil, registry.ErrNotFound
}
m.RUnlock()
return s, nil
}
func (m *Registry) ListServices() ([]*registry.Service, error) {
m.RLock()
var services []*registry.Service
for _, service := range m.Services {
services = append(services, service...)
}
m.RUnlock()
return services, nil
}
func (m *Registry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
go m.watch(&registry.Result{Action: "update", Service: s})
m.Lock()
services := addServices(m.Services[s.Name], []*registry.Service{s})
m.Services[s.Name] = services
m.Unlock()
return nil
}
func (m *Registry) Deregister(s *registry.Service) error {
go m.watch(&registry.Result{Action: "delete", Service: s})
m.Lock()
services := delServices(m.Services[s.Name], []*registry.Service{s})
m.Services[s.Name] = services
m.Unlock()
return nil
}
func (m *Registry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
var wo registry.WatchOptions
for _, o := range opts {
o(&wo)
}
w := &Watcher{
exit: make(chan bool),
res: make(chan *registry.Result),
id: uuid.New().String(),
wo: wo,
}
m.Lock()
m.Watchers[w.id] = w
m.Unlock()
return w, nil
}
func (m *Registry) String() string {
return "memory"
}
func NewRegistry(opts ...registry.Option) registry.Registry {
options := registry.Options{
Context: context.Background(),
}
for _, o := range opts {
o(&options)
}
services := getServices(options.Context)
if services == nil {
services = make(map[string][]*registry.Service)
}
return &Registry{
options: options,
Services: services,
Watchers: make(map[string]*Watcher),
}
}

View File

@@ -1,4 +1,4 @@
package mock
package memory
import (
"testing"
@@ -80,7 +80,7 @@ var (
}
)
func TestMockRegistry(t *testing.T) {
func TestMemoryRegistry(t *testing.T) {
m := NewRegistry()
fn := func(k string, v []*registry.Service) {
@@ -107,11 +107,6 @@ func TestMockRegistry(t *testing.T) {
}
}
// test existing mock data
for k, v := range mockData {
fn(k, v)
}
// register data
for _, v := range testData {
for _, service := range v {
@@ -123,7 +118,6 @@ func TestMockRegistry(t *testing.T) {
// using test data
for k, v := range testData {
fn(k, v)
}

View File

@@ -1,4 +1,4 @@
package mock
package memory
import (
"errors"
@@ -6,12 +6,12 @@ import (
"github.com/micro/go-micro/registry"
)
type mockWatcher struct {
type memoryWatcher struct {
exit chan bool
opts registry.WatchOptions
}
func (m *mockWatcher) Next() (*registry.Result, error) {
func (m *memoryWatcher) Next() (*registry.Result, error) {
// not implement so we just block until exit
select {
case <-m.exit:
@@ -19,7 +19,7 @@ func (m *mockWatcher) Next() (*registry.Result, error) {
}
}
func (m *mockWatcher) Stop() {
func (m *memoryWatcher) Stop() {
select {
case <-m.exit:
return

View File

@@ -0,0 +1,27 @@
package memory
import (
"context"
"github.com/micro/go-micro/registry"
)
type servicesKey struct{}
func getServices(ctx context.Context) map[string][]*registry.Service {
s, ok := ctx.Value(servicesKey{}).(map[string][]*registry.Service)
if !ok {
return nil
}
return s
}
// Services is an option that preloads service data
func Services(s map[string][]*registry.Service) registry.Option {
return func(o *registry.Options) {
if o.Context == nil {
o.Context = context.Background()
}
o.Context = context.WithValue(o.Context, servicesKey{}, s)
}
}

View File

@@ -0,0 +1,37 @@
package memory
import (
"errors"
"github.com/micro/go-micro/registry"
)
type Watcher struct {
id string
wo registry.WatchOptions
res chan *registry.Result
exit chan bool
}
func (m *Watcher) Next() (*registry.Result, error) {
for {
select {
case r := <-m.res:
if len(m.wo.Service) > 0 && m.wo.Service != r.Service.Name {
continue
}
return r, nil
case <-m.exit:
return nil, errors.New("watcher stopped")
}
}
}
func (m *Watcher) Stop() {
select {
case <-m.exit:
return
default:
close(m.exit)
}
}

View File

@@ -0,0 +1,30 @@
package memory
import (
"testing"
"github.com/micro/go-micro/registry"
)
func TestWatcher(t *testing.T) {
w := &Watcher{
id: "test",
res: make(chan *registry.Result),
exit: make(chan bool),
}
go func() {
w.res <- &registry.Result{}
}()
_, err := w.Next()
if err != nil {
t.Fatal("unexpected err", err)
}
w.Stop()
if _, err := w.Next(); err == nil {
t.Fatal("expected error on Next()")
}
}

View File

@@ -1,115 +0,0 @@
// Package mock provides a mock registry for testing
package mock
import (
"github.com/micro/go-micro/registry"
)
type mockRegistry struct {
Services map[string][]*registry.Service
}
var (
mockData = map[string][]*registry.Service{
"foo": []*registry.Service{
{
Name: "foo",
Version: "1.0.0",
Nodes: []*registry.Node{
{
Id: "foo-1.0.0-123",
Address: "localhost",
Port: 9999,
},
{
Id: "foo-1.0.0-321",
Address: "localhost",
Port: 9999,
},
},
},
{
Name: "foo",
Version: "1.0.1",
Nodes: []*registry.Node{
{
Id: "foo-1.0.1-321",
Address: "localhost",
Port: 6666,
},
},
},
{
Name: "foo",
Version: "1.0.3",
Nodes: []*registry.Node{
{
Id: "foo-1.0.3-345",
Address: "localhost",
Port: 8888,
},
},
},
},
}
)
func (m *mockRegistry) init() {
// add some mock data
m.Services = mockData
}
func (m *mockRegistry) GetService(service string) ([]*registry.Service, error) {
s, ok := m.Services[service]
if !ok || len(s) == 0 {
return nil, registry.ErrNotFound
}
return s, nil
}
func (m *mockRegistry) ListServices() ([]*registry.Service, error) {
var services []*registry.Service
for _, service := range m.Services {
services = append(services, service...)
}
return services, nil
}
func (m *mockRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
services := addServices(m.Services[s.Name], []*registry.Service{s})
m.Services[s.Name] = services
return nil
}
func (m *mockRegistry) Deregister(s *registry.Service) error {
services := delServices(m.Services[s.Name], []*registry.Service{s})
m.Services[s.Name] = services
return nil
}
func (m *mockRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
var wopts registry.WatchOptions
for _, o := range opts {
o(&wopts)
}
return &mockWatcher{exit: make(chan bool), opts: wopts}, nil
}
func (m *mockRegistry) String() string {
return "mock"
}
func (m *mockRegistry) Init(opts ...registry.Option) error {
return nil
}
func (m *mockRegistry) Options() registry.Options {
return registry.Options{}
}
func NewRegistry(opts ...registry.Options) registry.Registry {
m := &mockRegistry{Services: make(map[string][]*registry.Service)}
m.init()
return m
}

View File

@@ -11,7 +11,6 @@ type Options struct {
Timeout time.Duration
Secure bool
TLSConfig *tls.Config
// Other options for implementations of the interface
// can be stored in a context
Context context.Context

View File

@@ -26,7 +26,7 @@ type RegisterOption func(*RegisterOptions)
type WatchOption func(*WatchOptions)
var (
DefaultRegistry = newConsulRegistry()
DefaultRegistry = NewRegistry()
// Not found error when GetService is called
ErrNotFound = errors.New("not found")
@@ -34,10 +34,6 @@ var (
ErrWatcherStopped = errors.New("watcher stopped")
)
func NewRegistry(opts ...Option) Registry {
return newConsulRegistry(opts...)
}
// Register a service node. Additionally supply options such as TTL.
func Register(s *Service, opts ...RegisterOption) error {
return DefaultRegistry.Register(s, opts...)

View File

@@ -1,18 +1,16 @@
package mdns
package registry
import (
"testing"
"github.com/micro/go-micro/registry"
)
func TestWatcher(t *testing.T) {
testData := []*registry.Service{
&registry.Service{
testData := []*Service{
&Service{
Name: "test1",
Version: "1.0.1",
Nodes: []*registry.Node{
&registry.Node{
Nodes: []*Node{
&Node{
Id: "test1-1",
Address: "10.0.0.1",
Port: 10001,
@@ -22,11 +20,11 @@ func TestWatcher(t *testing.T) {
},
},
},
&registry.Service{
&Service{
Name: "test2",
Version: "1.0.2",
Nodes: []*registry.Node{
&registry.Node{
Nodes: []*Node{
&Node{
Id: "test2-1",
Address: "10.0.0.2",
Port: 10002,
@@ -36,11 +34,11 @@ func TestWatcher(t *testing.T) {
},
},
},
&registry.Service{
&Service{
Name: "test3",
Version: "1.0.3",
Nodes: []*registry.Node{
&registry.Node{
Nodes: []*Node{
&Node{
Id: "test3-1",
Address: "10.0.0.3",
Port: 10003,
@@ -52,7 +50,7 @@ func TestWatcher(t *testing.T) {
},
}
testFn := func(service, s *registry.Service) {
testFn := func(service, s *Service) {
if s == nil {
t.Fatalf("Expected one result for %s got nil", service.Name)

View File

@@ -1,314 +1,25 @@
package selector
import (
"sync"
"time"
"github.com/micro/go-log"
"github.com/micro/go-micro/registry"
"github.com/micro/go-rcache"
)
type registrySelector struct {
so Options
ttl time.Duration
// registry cache
sync.RWMutex
cache map[string][]*registry.Service
ttls map[string]time.Time
watched map[string]bool
// used to close or reload watcher
reload chan bool
exit chan bool
so Options
rc rcache.Cache
}
var (
DefaultTTL = time.Minute
)
func (c *registrySelector) quit() bool {
select {
case <-c.exit:
return true
default:
return false
}
}
// cp copies a service. Because we're caching handing back pointers would
// create a race condition, so we do this instead
// its fast enough
func (c *registrySelector) cp(current []*registry.Service) []*registry.Service {
var services []*registry.Service
for _, service := range current {
// copy service
s := new(registry.Service)
*s = *service
// copy nodes
var nodes []*registry.Node
for _, node := range service.Nodes {
n := new(registry.Node)
*n = *node
nodes = append(nodes, n)
}
s.Nodes = nodes
// copy endpoints
var eps []*registry.Endpoint
for _, ep := range service.Endpoints {
e := new(registry.Endpoint)
*e = *ep
eps = append(eps, e)
}
s.Endpoints = eps
// append service
services = append(services, s)
}
return services
}
func (c *registrySelector) del(service string) {
delete(c.cache, service)
delete(c.ttls, service)
}
func (c *registrySelector) get(service string) ([]*registry.Service, error) {
// read lock
c.RLock()
// check the cache first
services, ok := c.cache[service]
// get cache ttl
ttl, kk := c.ttls[service]
// got services && within ttl so return cache
if ok && kk && time.Since(ttl) < c.ttl {
// make a copy
cp := c.cp(services)
// unlock the read
c.RUnlock()
// return servics
return cp, nil
}
// get does the actual request for a service and cache it
get := func(service string) ([]*registry.Service, error) {
// ask the registry
services, err := c.so.Registry.GetService(service)
if err != nil {
return nil, err
}
// cache results
c.Lock()
c.set(service, c.cp(services))
c.Unlock()
return services, nil
}
// watch service if not watched
if _, ok := c.watched[service]; !ok {
go c.run(service)
}
// unlock the read lock
c.RUnlock()
// get and return services
return get(service)
}
func (c *registrySelector) set(service string, services []*registry.Service) {
c.cache[service] = services
c.ttls[service] = time.Now().Add(c.ttl)
}
func (c *registrySelector) update(res *registry.Result) {
if res == nil || res.Service == nil {
return
}
c.Lock()
defer c.Unlock()
services, ok := c.cache[res.Service.Name]
if !ok {
// we're not going to cache anything
// unless there was already a lookup
return
}
if len(res.Service.Nodes) == 0 {
switch res.Action {
case "delete":
c.del(res.Service.Name)
}
return
}
// existing service found
var service *registry.Service
var index int
for i, s := range services {
if s.Version == res.Service.Version {
service = s
index = i
func (c *registrySelector) newRCache() rcache.Cache {
ropts := []rcache.Option{}
if c.so.Context != nil {
if t, ok := c.so.Context.Value("selector_ttl").(time.Duration); ok {
ropts = append(ropts, rcache.WithTTL(t))
}
}
switch res.Action {
case "create", "update":
if service == nil {
c.set(res.Service.Name, append(services, res.Service))
return
}
// append old nodes to new service
for _, cur := range service.Nodes {
var seen bool
for _, node := range res.Service.Nodes {
if cur.Id == node.Id {
seen = true
break
}
}
if !seen {
res.Service.Nodes = append(res.Service.Nodes, cur)
}
}
services[index] = res.Service
c.set(res.Service.Name, services)
case "delete":
if service == nil {
return
}
var nodes []*registry.Node
// filter cur nodes to remove the dead one
for _, cur := range service.Nodes {
var seen bool
for _, del := range res.Service.Nodes {
if del.Id == cur.Id {
seen = true
break
}
}
if !seen {
nodes = append(nodes, cur)
}
}
// still got nodes, save and return
if len(nodes) > 0 {
service.Nodes = nodes
services[index] = service
c.set(service.Name, services)
return
}
// zero nodes left
// only have one thing to delete
// nuke the thing
if len(services) == 1 {
c.del(service.Name)
return
}
// still have more than 1 service
// check the version and keep what we know
var srvs []*registry.Service
for _, s := range services {
if s.Version != service.Version {
srvs = append(srvs, s)
}
}
// save
c.set(service.Name, srvs)
}
}
// run starts the cache watcher loop
// it creates a new watcher if there's a problem
// reloads the watcher if Init is called
// and returns when Close is called
func (c *registrySelector) run(name string) {
// set watcher
c.Lock()
c.watched[name] = true
c.Unlock()
// delete watcher on exit
defer func() {
c.Lock()
delete(c.watched, name)
c.Unlock()
}()
for {
// exit early if already dead
if c.quit() {
return
}
// create new watcher
w, err := c.so.Registry.Watch(
registry.WatchService(name),
)
if err != nil {
if c.quit() {
return
}
log.Log(err)
time.Sleep(time.Second)
continue
}
// watch for events
if err := c.watch(w); err != nil {
if c.quit() {
return
}
log.Log(err)
continue
}
}
}
// watch loops the next event and calls update
// it returns if there's an error
func (c *registrySelector) watch(w registry.Watcher) error {
defer w.Stop()
// manage this loop
go func() {
// wait for exit or reload signal
select {
case <-c.exit:
case <-c.reload:
}
// stop the watcher
w.Stop()
}()
for {
res, err := w.Next()
if err != nil {
return err
}
c.update(res)
}
return rcache.New(c.so.Registry, ropts...)
}
func (c *registrySelector) Init(opts ...Option) error {
@@ -316,15 +27,8 @@ func (c *registrySelector) Init(opts ...Option) error {
o(&c.so)
}
// reload the watcher
go func() {
select {
case <-c.exit:
return
default:
c.reload <- true
}
}()
c.rc.Stop()
c.rc = c.newRCache()
return nil
}
@@ -345,7 +49,7 @@ func (c *registrySelector) Select(service string, opts ...SelectOption) (Next, e
// get the service
// try the cache first
// if that fails go directly to the registry
services, err := c.get(service)
services, err := c.rc.GetService(service)
if err != nil {
return nil, err
}
@@ -371,17 +75,8 @@ func (c *registrySelector) Reset(service string) {
// Close stops the watcher and destroys the cache
func (c *registrySelector) Close() error {
c.Lock()
c.cache = make(map[string][]*registry.Service)
c.watched = make(map[string]bool)
c.Unlock()
c.rc.Stop()
select {
case <-c.exit:
return nil
default:
close(c.exit)
}
return nil
}
@@ -402,21 +97,10 @@ func NewSelector(opts ...Option) Selector {
sopts.Registry = registry.DefaultRegistry
}
ttl := DefaultTTL
if sopts.Context != nil {
if t, ok := sopts.Context.Value("selector_ttl").(time.Duration); ok {
ttl = t
}
s := &registrySelector{
so: sopts,
}
s.rc = s.newRCache()
return &registrySelector{
so: sopts,
ttl: ttl,
watched: make(map[string]bool),
cache: make(map[string][]*registry.Service),
ttls: make(map[string]time.Time),
reload: make(chan bool, 1),
exit: make(chan bool),
}
return s
}

View File

@@ -3,13 +3,15 @@ package selector
import (
"testing"
"github.com/micro/go-micro/registry/mock"
"github.com/micro/go-micro/registry/memory"
)
func TestRegistrySelector(t *testing.T) {
counts := map[string]int{}
cache := NewSelector(Registry(mock.NewRegistry()))
r := memory.NewRegistry()
r.(*memory.Registry).Setup()
cache := NewSelector(Registry(r))
next, err := cache.Select("foo")
if err != nil {

128
selector/dns/dns.go Normal file
View File

@@ -0,0 +1,128 @@
// Package dns provides a dns SRV selector
package dns
import (
"net"
"strconv"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/selector"
)
type dnsSelector struct {
options selector.Options
domain string
}
var (
DefaultDomain = "local"
)
func (d *dnsSelector) Init(opts ...selector.Option) error {
for _, o := range opts {
o(&d.options)
}
return nil
}
func (d *dnsSelector) Options() selector.Options {
return d.options
}
func (d *dnsSelector) Select(service string, opts ...selector.SelectOption) (selector.Next, error) {
var srv []*net.SRV
// check if its host:port
host, port, err := net.SplitHostPort(service)
// not host:port
if err != nil {
// lookup the SRV record
_, srvs, err := net.LookupSRV(service, "tcp", d.domain)
if err != nil {
return nil, err
}
// set SRV records
srv = srvs
// got host:port
} else {
p, _ := strconv.Atoi(port)
// lookup the A record
ips, err := net.LookupHost(host)
if err != nil {
return nil, err
}
// create SRV records
for _, ip := range ips {
srv = append(srv, &net.SRV{
Target: ip,
Port: uint16(p),
})
}
}
var nodes []*registry.Node
for _, node := range srv {
nodes = append(nodes, &registry.Node{
Id: node.Target,
Address: node.Target,
Port: int(node.Port),
})
}
services := []*registry.Service{
&registry.Service{
Name: service,
Nodes: nodes,
},
}
sopts := selector.SelectOptions{
Strategy: d.options.Strategy,
}
for _, opt := range opts {
opt(&sopts)
}
// apply the filters
for _, filter := range sopts.Filters {
services = filter(services)
}
// if there's nothing left, return
if len(services) == 0 {
return nil, selector.ErrNoneAvailable
}
return sopts.Strategy(services), nil
}
func (d *dnsSelector) Mark(service string, node *registry.Node, err error) {
return
}
func (d *dnsSelector) Reset(service string) {
return
}
func (d *dnsSelector) Close() error {
return nil
}
func (d *dnsSelector) String() string {
return "dns"
}
func NewSelector(opts ...selector.Option) selector.Selector {
options := selector.Options{
Strategy: selector.Random,
}
for _, o := range opts {
o(&options)
}
return &dnsSelector{options: options, domain: DefaultDomain}
}

View File

@@ -1,4 +1,4 @@
// Package registry is uses the go-micro registry for selection
// Package registry uses the go-micro registry for selection
package registry
import (

View File

@@ -1,4 +1,4 @@
// Package selector is a way to load balance service nodes
// Package selector is a way to pick a list of service nodes
package selector
import (

71
selector/static/static.go Normal file
View File

@@ -0,0 +1,71 @@
// Package static provides a static resolver which returns the name/ip passed in without any change
package static
import (
"net"
"strconv"
"github.com/micro/go-micro/registry"
"github.com/micro/go-micro/selector"
)
// staticSelector is a static selector
type staticSelector struct {
opts selector.Options
}
func (s *staticSelector) Init(opts ...selector.Option) error {
for _, o := range opts {
o(&s.opts)
}
return nil
}
func (s *staticSelector) Options() selector.Options {
return s.opts
}
func (s *staticSelector) Select(service string, opts ...selector.SelectOption) (selector.Next, error) {
var port int
addr, pt, err := net.SplitHostPort(service)
if err != nil {
addr = service
port = 0
} else {
port, _ = strconv.Atoi(pt)
}
return func() (*registry.Node, error) {
return &registry.Node{
Id: service,
Address: addr,
Port: port,
}, nil
}, nil
}
func (s *staticSelector) Mark(service string, node *registry.Node, err error) {
return
}
func (s *staticSelector) Reset(service string) {
return
}
func (s *staticSelector) Close() error {
return nil
}
func (s *staticSelector) String() string {
return "static"
}
func NewSelector(opts ...selector.Option) selector.Selector {
var options selector.Options
for _, o := range opts {
o(&options)
}
return &staticSelector{
opts: options,
}
}

View File

@@ -1,13 +1,20 @@
package server
import "context"
type HandlerOption func(*HandlerOptions)
type HandlerOptions struct {
Internal bool
Metadata map[string]map[string]string
}
type SubscriberOption func(*SubscriberOptions)
type SubscriberOptions struct {
Queue string
Internal bool
Context context.Context
}
// EndpointMetadata is a Handler option that allows metadata to be added to
@@ -34,6 +41,17 @@ func InternalSubscriber(b bool) SubscriberOption {
o.Internal = b
}
}
func NewSubscriberOptions(opts ...SubscriberOption) SubscriberOptions {
opt := SubscriberOptions{
Context: context.Background(),
}
for _, o := range opts {
o(&opt)
}
return opt
}
// Shared queue name distributed messages across subscribers
func SubscriberQueue(n string) SubscriberOption {
@@ -41,3 +59,10 @@ func SubscriberQueue(n string) SubscriberOption {
o.Queue = n
}
}
// SubscriberContext set context options to allow broker SubscriberOption passed
func SubscriberContext(ctx context.Context) SubscriberOption {
return func(o *SubscriberOptions) {
o.Context = ctx
}
}

View File

@@ -25,7 +25,13 @@ type Options struct {
HdlrWrappers []HandlerWrapper
SubWrappers []SubscriberWrapper
// The register expiry time
RegisterTTL time.Duration
// The interval on which to register
RegisterInterval time.Duration
// The router for requests
Router Router
// Debug Handler which can be set by a user
DebugHandler debug.DebugHandler
@@ -164,6 +170,20 @@ func RegisterTTL(t time.Duration) Option {
}
}
// Register the service with at interval
func RegisterInterval(t time.Duration) Option {
return func(o *Options) {
o.RegisterInterval = t
}
}
// WithRouter sets the request router
func WithRouter(r Router) Option {
return func(o *Options) {
o.Router = r
}
}
// Wait tells the server to wait for requests to finish before exiting
func Wait(b bool) Option {
return func(o *Options) {

View File

@@ -4,7 +4,11 @@ import (
"bytes"
"github.com/micro/go-micro/codec"
raw "github.com/micro/go-micro/codec/bytes"
"github.com/micro/go-micro/codec/grpc"
"github.com/micro/go-micro/codec/json"
"github.com/micro/go-micro/codec/jsonrpc"
"github.com/micro/go-micro/codec/proto"
"github.com/micro/go-micro/codec/protorpc"
"github.com/micro/go-micro/transport"
"github.com/pkg/errors"
@@ -13,6 +17,7 @@ import (
type rpcCodec struct {
socket transport.Socket
codec codec.Codec
first bool
req *transport.Message
buf *readWriteCloser
@@ -24,6 +29,20 @@ type readWriteCloser struct {
}
var (
DefaultContentType = "application/protobuf"
DefaultCodecs = map[string]codec.NewCodec{
"application/grpc": grpc.NewCodec,
"application/grpc+json": grpc.NewCodec,
"application/grpc+proto": grpc.NewCodec,
"application/json": json.NewCodec,
"application/json-rpc": jsonrpc.NewCodec,
"application/protobuf": proto.NewCodec,
"application/proto-rpc": protorpc.NewCodec,
"application/octet-stream": raw.NewCodec,
}
// TODO: remove legacy codec list
defaultCodecs = map[string]codec.NewCodec{
"application/json": jsonrpc.NewCodec,
"application/json-rpc": jsonrpc.NewCodec,
@@ -47,12 +66,99 @@ func (rwc *readWriteCloser) Close() error {
return nil
}
func newRpcCodec(req *transport.Message, socket transport.Socket, c codec.NewCodec) serverCodec {
func getHeader(hdr string, md map[string]string) string {
if hd := md[hdr]; len(hd) > 0 {
return hd
}
return md["X-"+hdr]
}
func getHeaders(m *codec.Message) {
get := func(hdr, v string) string {
if len(v) > 0 {
return v
}
if hd := m.Header[hdr]; len(hd) > 0 {
return hd
}
// old
return m.Header["X-"+hdr]
}
m.Id = get("Micro-Id", m.Id)
m.Error = get("Micro-Error", m.Error)
m.Endpoint = get("Micro-Endpoint", m.Endpoint)
m.Method = get("Micro-Method", m.Method)
m.Target = get("Micro-Service", m.Target)
// TODO: remove this cruft
if len(m.Endpoint) == 0 {
m.Endpoint = m.Method
}
}
func setHeaders(m, r *codec.Message) {
set := func(hdr, v string) {
if len(v) == 0 {
return
}
m.Header[hdr] = v
m.Header["X-"+hdr] = v
}
// set headers
set("Micro-Id", r.Id)
set("Micro-Service", r.Target)
set("Micro-Method", r.Method)
set("Micro-Endpoint", r.Endpoint)
set("Micro-Error", r.Error)
}
// setupProtocol sets up the old protocol
func setupProtocol(msg *transport.Message) codec.NewCodec {
service := getHeader("Micro-Service", msg.Header)
method := getHeader("Micro-Method", msg.Header)
endpoint := getHeader("Micro-Endpoint", msg.Header)
protocol := getHeader("Micro-Protocol", msg.Header)
target := getHeader("Micro-Target", msg.Header)
// if the protocol exists (mucp) do nothing
if len(protocol) > 0 {
return nil
}
// if no service/method/endpoint then it's the old protocol
if len(service) == 0 && len(method) == 0 && len(endpoint) == 0 {
return defaultCodecs[msg.Header["Content-Type"]]
}
// old target method specified
if len(target) > 0 {
return defaultCodecs[msg.Header["Content-Type"]]
}
// no method then set to endpoint
if len(method) == 0 {
msg.Header["Micro-Method"] = endpoint
}
// no endpoint then set to method
if len(endpoint) == 0 {
msg.Header["Micro-Endpoint"] = method
}
return nil
}
func newRpcCodec(req *transport.Message, socket transport.Socket, c codec.NewCodec) codec.Codec {
rwc := &readWriteCloser{
rbuf: bytes.NewBuffer(req.Body),
wbuf: bytes.NewBuffer(nil),
}
r := &rpcCodec{
first: true,
buf: rwc,
codec: c(rwc),
req: req,
@@ -61,53 +167,129 @@ func newRpcCodec(req *transport.Message, socket transport.Socket, c codec.NewCod
return r
}
func (c *rpcCodec) ReadRequestHeader(r *request, first bool) error {
m := codec.Message{Header: c.req.Header}
func (c *rpcCodec) ReadHeader(r *codec.Message, t codec.MessageType) error {
// the initial message
m := codec.Message{
Header: c.req.Header,
Body: c.req.Body,
}
if !first {
// if its a follow on request read it
if !c.first {
var tm transport.Message
// read off the socket
if err := c.socket.Recv(&tm); err != nil {
return err
}
// reset the read buffer
c.buf.rbuf.Reset()
// write the body to the buffer
if _, err := c.buf.rbuf.Write(tm.Body); err != nil {
return err
}
// set the message header
m.Header = tm.Header
// set the message body
m.Body = tm.Body
// set req
c.req = &tm
}
err := c.codec.ReadHeader(&m, codec.Request)
r.ServiceMethod = m.Method
r.Seq = m.Id
return err
// no longer first read
c.first = false
// set some internal things
getHeaders(&m)
// read header via codec
if err := c.codec.ReadHeader(&m, codec.Request); err != nil {
return err
}
// fallback for 0.14 and older
if len(m.Endpoint) == 0 {
m.Endpoint = m.Method
}
// set message
*r = m
return nil
}
func (c *rpcCodec) ReadRequestBody(b interface{}) error {
func (c *rpcCodec) ReadBody(b interface{}) error {
// don't read empty body
if len(c.req.Body) == 0 {
return nil
}
// read raw data
if v, ok := b.(*raw.Frame); ok {
v.Data = c.req.Body
return nil
}
// decode the usual way
return c.codec.ReadBody(b)
}
func (c *rpcCodec) WriteResponse(r *response, body interface{}, last bool) error {
func (c *rpcCodec) Write(r *codec.Message, b interface{}) error {
c.buf.wbuf.Reset()
// create a new message
m := &codec.Message{
Method: r.ServiceMethod,
Id: r.Seq,
Error: r.Error,
Type: codec.Response,
Header: map[string]string{},
Target: r.Target,
Method: r.Method,
Endpoint: r.Endpoint,
Id: r.Id,
Error: r.Error,
Type: r.Type,
Header: r.Header,
}
if err := c.codec.Write(m, body); err != nil {
if m.Header == nil {
m.Header = map[string]string{}
}
setHeaders(m, r)
// the body being sent
var body []byte
// is it a raw frame?
if v, ok := b.(*raw.Frame); ok {
body = v.Data
// if we have encoded data just send it
} else if len(r.Body) > 0 {
body = r.Body
// write the body to codec
} else if err := c.codec.Write(m, b); err != nil {
c.buf.wbuf.Reset()
// write an error if it failed
m.Error = errors.Wrapf(err, "Unable to encode body").Error()
m.Header["X-Micro-Error"] = m.Error
m.Header["Micro-Error"] = m.Error
// no body to write
if err := c.codec.Write(m, nil); err != nil {
return err
}
} else {
// set the body
body = c.buf.wbuf.Bytes()
}
m.Header["Content-Type"] = c.req.Header["Content-Type"]
// Set content type if theres content
if len(body) > 0 {
m.Header["Content-Type"] = c.req.Header["Content-Type"]
}
// send on the socket
return c.socket.Send(&transport.Message{
Header: m.Header,
Body: c.buf.wbuf.Bytes(),
Body: body,
})
}
@@ -116,3 +298,7 @@ func (c *rpcCodec) Close() error {
c.codec.Close()
return c.socket.Close()
}
func (c *rpcCodec) String() string {
return "rpc"
}

View File

@@ -47,15 +47,14 @@ func TestCodecWriteError(t *testing.T) {
socket: socket,
}
err := c.WriteResponse(&response{
ServiceMethod: "Service.Method",
Seq: 0,
Error: "",
next: nil,
}, "body", false)
err := c.Write(&codec.Message{
Endpoint: "Service.Endpoint",
Id: "0",
Error: "",
}, "body")
if err != nil {
t.Fatalf(`Expected WriteResponse to fail; got "%+v" instead`, err)
t.Fatalf(`Expected Write to fail; got "%+v" instead`, err)
}
const expectedError = "Unable to encode body: simulating a codec write failure"

View File

@@ -1,10 +1,19 @@
package server
import (
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/transport"
)
type rpcRequest struct {
service string
method string
endpoint string
contentType string
request interface{}
socket transport.Socket
codec codec.Codec
header map[string]string
body []byte
stream bool
}
@@ -14,6 +23,10 @@ type rpcMessage struct {
payload interface{}
}
func (r *rpcRequest) Codec() codec.Reader {
return r.codec
}
func (r *rpcRequest) ContentType() string {
return r.contentType
}
@@ -26,8 +39,35 @@ func (r *rpcRequest) Method() string {
return r.method
}
func (r *rpcRequest) Request() interface{} {
return r.request
func (r *rpcRequest) Endpoint() string {
return r.endpoint
}
func (r *rpcRequest) Header() map[string]string {
return r.header
}
func (r *rpcRequest) Body() interface{} {
// TODO: convert to interface value
return r.body
}
func (r *rpcRequest) Read() ([]byte, error) {
// got a body
if r.body != nil {
b := r.body
r.body = nil
return b, nil
}
var msg transport.Message
err := r.socket.Recv(&msg)
if err != nil {
return nil, err
}
r.header = msg.Header
return msg.Body, nil
}
func (r *rpcRequest) Stream() bool {

35
server/rpc_response.go Normal file
View File

@@ -0,0 +1,35 @@
package server
import (
"net/http"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/transport"
)
type rpcResponse struct {
header map[string]string
socket transport.Socket
codec codec.Codec
}
func (r *rpcResponse) Codec() codec.Writer {
return r.codec
}
func (r *rpcResponse) WriteHeader(hdr map[string]string) {
for k, v := range hdr {
r.header[k] = v
}
}
func (r *rpcResponse) Write(b []byte) error {
if _, ok := r.header["Content-Type"]; !ok {
r.header["Content-Type"] = http.DetectContentType(b)
}
return r.socket.Send(&transport.Message{
Header: r.header,
Body: b,
})
}

View File

@@ -17,6 +17,7 @@ import (
"unicode/utf8"
"github.com/micro/go-log"
"github.com/micro/go-micro/codec"
)
var (
@@ -48,20 +49,17 @@ type service struct {
}
type request struct {
ServiceMethod string // format: "Service.Method"
Seq uint64 // sequence number chosen by client
next *request // for free list in Server
msg *codec.Message
next *request // for free list in Server
}
type response struct {
ServiceMethod string // echoes that of the Request
Seq uint64 // echoes that of the request
Error string // error, if any.
next *response // for free list in Server
msg *codec.Message
next *response // for free list in Server
}
// server represents an RPC Server.
type server struct {
// router represents an RPC router.
type router struct {
name string
mu sync.Mutex // protects the serviceMap
serviceMap map[string]*service
@@ -72,6 +70,12 @@ type server struct {
hdlrWrappers []HandlerWrapper
}
func newRpcRouter() *router {
return &router{
serviceMap: make(map[string]*service),
}
}
// Is this an exported - upper case - name?
func isExported(name string) bool {
rune, _ := utf8.DecodeRuneInString(name)
@@ -158,78 +162,37 @@ func prepareMethod(method reflect.Method) *methodType {
return &methodType{method: method, ArgType: argType, ReplyType: replyType, ContextType: contextType, stream: stream}
}
func (server *server) register(rcvr interface{}) error {
server.mu.Lock()
defer server.mu.Unlock()
if server.serviceMap == nil {
server.serviceMap = make(map[string]*service)
}
s := new(service)
s.typ = reflect.TypeOf(rcvr)
s.rcvr = reflect.ValueOf(rcvr)
sname := reflect.Indirect(s.rcvr).Type().Name()
if sname == "" {
log.Fatal("rpc: no service name for type", s.typ.String())
}
if !isExported(sname) {
s := "rpc Register: type " + sname + " is not exported"
log.Log(s)
return errors.New(s)
}
if _, present := server.serviceMap[sname]; present {
return errors.New("rpc: service already defined: " + sname)
}
s.name = sname
s.method = make(map[string]*methodType)
func (router *router) sendResponse(sending sync.Locker, req *request, reply interface{}, cc codec.Writer, last bool) error {
msg := new(codec.Message)
msg.Type = codec.Response
resp := router.getResponse()
resp.msg = msg
// Install the methods
for m := 0; m < s.typ.NumMethod(); m++ {
method := s.typ.Method(m)
if mt := prepareMethod(method); mt != nil {
s.method[method.Name] = mt
}
}
if len(s.method) == 0 {
s := "rpc Register: type " + sname + " has no exported methods of suitable type"
log.Log(s)
return errors.New(s)
}
server.serviceMap[s.name] = s
return nil
}
func (server *server) sendResponse(sending sync.Locker, req *request, reply interface{}, codec serverCodec, errmsg string, last bool) (err error) {
resp := server.getResponse()
// Encode the response header
resp.ServiceMethod = req.ServiceMethod
if errmsg != "" {
resp.Error = errmsg
reply = invalidRequest
}
resp.Seq = req.Seq
resp.msg.Id = req.msg.Id
sending.Lock()
err = codec.WriteResponse(resp, reply, last)
err := cc.Write(resp.msg, reply)
sending.Unlock()
server.freeResponse(resp)
router.freeResponse(resp)
return err
}
func (s *service) call(ctx context.Context, server *server, sending *sync.Mutex, mtype *methodType, req *request, argv, replyv reflect.Value, codec serverCodec, ct string) {
func (s *service) call(ctx context.Context, router *router, sending *sync.Mutex, mtype *methodType, req *request, argv, replyv reflect.Value, cc codec.Writer) error {
defer router.freeRequest(req)
function := mtype.method.Func
var returnValues []reflect.Value
r := &rpcRequest{
service: server.name,
contentType: ct,
method: req.ServiceMethod,
service: req.msg.Target,
contentType: req.msg.Header["Content-Type"],
method: req.msg.Method,
endpoint: req.msg.Endpoint,
body: req.msg.Body,
}
if !mtype.stream {
r.request = argv.Interface()
fn := func(ctx context.Context, req Request, rsp interface{}) error {
returnValues = function.Call([]reflect.Value{s.rcvr, mtype.prepareContext(ctx), reflect.ValueOf(req.Request()), reflect.ValueOf(rsp)})
returnValues = function.Call([]reflect.Value{s.rcvr, mtype.prepareContext(ctx), reflect.ValueOf(argv.Interface()), reflect.ValueOf(rsp)})
// The return value for the method is an error.
if err := returnValues[0].Interface(); err != nil {
@@ -239,22 +202,13 @@ func (s *service) call(ctx context.Context, server *server, sending *sync.Mutex,
return nil
}
for i := len(server.hdlrWrappers); i > 0; i-- {
fn = server.hdlrWrappers[i-1](fn)
// execute handler
if err := fn(ctx, r, replyv.Interface()); err != nil {
return err
}
errmsg := ""
err := fn(ctx, r, replyv.Interface())
if err != nil {
errmsg = err.Error()
}
err = server.sendResponse(sending, req, replyv.Interface(), codec, errmsg, true)
if err != nil {
log.Log("rpc call: unable to send response: ", err)
}
server.freeRequest(req)
return
// send response
return router.sendResponse(sending, req, replyv.Interface(), cc, true)
}
// declare a local error to see if we errored out already
@@ -264,9 +218,9 @@ func (s *service) call(ctx context.Context, server *server, sending *sync.Mutex,
stream := &rpcStream{
context: ctx,
codec: codec,
codec: cc.(codec.Codec),
request: r,
seq: req.Seq,
id: req.msg.Id,
}
// Invoke the method, providing a new value for the reply.
@@ -284,23 +238,18 @@ func (s *service) call(ctx context.Context, server *server, sending *sync.Mutex,
}
}
for i := len(server.hdlrWrappers); i > 0; i-- {
fn = server.hdlrWrappers[i-1](fn)
}
// client.Stream request
r.stream = true
errmsg := ""
// execute handler
if err := fn(ctx, r, stream); err != nil {
errmsg = err.Error()
return err
}
// this is the last packet, we don't do anything with
// the error here (well sendStreamResponse will log it
// already)
server.sendResponse(sending, req, nil, codec, errmsg, true)
server.freeRequest(req)
return router.sendResponse(sending, req, nil, cc, true)
}
func (m *methodType) prepareContext(ctx context.Context) reflect.Value {
@@ -310,77 +259,61 @@ func (m *methodType) prepareContext(ctx context.Context) reflect.Value {
return reflect.Zero(m.ContextType)
}
func (server *server) serveRequest(ctx context.Context, codec serverCodec, ct string) error {
sending := new(sync.Mutex)
service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
if err != nil {
if !keepReading {
return err
}
// send a response if we actually managed to read a header.
if req != nil {
server.sendResponse(sending, req, invalidRequest, codec, err.Error(), true)
server.freeRequest(req)
}
return err
}
service.call(ctx, server, sending, mtype, req, argv, replyv, codec, ct)
return nil
}
func (server *server) getRequest() *request {
server.reqLock.Lock()
req := server.freeReq
func (router *router) getRequest() *request {
router.reqLock.Lock()
req := router.freeReq
if req == nil {
req = new(request)
} else {
server.freeReq = req.next
router.freeReq = req.next
*req = request{}
}
server.reqLock.Unlock()
router.reqLock.Unlock()
return req
}
func (server *server) freeRequest(req *request) {
server.reqLock.Lock()
req.next = server.freeReq
server.freeReq = req
server.reqLock.Unlock()
func (router *router) freeRequest(req *request) {
router.reqLock.Lock()
req.next = router.freeReq
router.freeReq = req
router.reqLock.Unlock()
}
func (server *server) getResponse() *response {
server.respLock.Lock()
resp := server.freeResp
func (router *router) getResponse() *response {
router.respLock.Lock()
resp := router.freeResp
if resp == nil {
resp = new(response)
} else {
server.freeResp = resp.next
router.freeResp = resp.next
*resp = response{}
}
server.respLock.Unlock()
router.respLock.Unlock()
return resp
}
func (server *server) freeResponse(resp *response) {
server.respLock.Lock()
resp.next = server.freeResp
server.freeResp = resp
server.respLock.Unlock()
func (router *router) freeResponse(resp *response) {
router.respLock.Lock()
resp.next = router.freeResp
router.freeResp = resp
router.respLock.Unlock()
}
func (server *server) readRequest(codec serverCodec) (service *service, mtype *methodType, req *request, argv, replyv reflect.Value, keepReading bool, err error) {
service, mtype, req, keepReading, err = server.readRequestHeader(codec)
func (router *router) readRequest(r Request) (service *service, mtype *methodType, req *request, argv, replyv reflect.Value, keepReading bool, err error) {
cc := r.Codec()
service, mtype, req, keepReading, err = router.readHeader(cc)
if err != nil {
if !keepReading {
return
}
// discard body
codec.ReadRequestBody(nil)
cc.ReadBody(nil)
return
}
// is it a streaming request? then we don't read the body
if mtype.stream {
codec.ReadRequestBody(nil)
cc.ReadBody(nil)
return
}
@@ -393,7 +326,7 @@ func (server *server) readRequest(codec serverCodec) (service *service, mtype *m
argIsValue = true
}
// argv guaranteed to be a pointer now.
if err = codec.ReadRequestBody(argv.Interface()); err != nil {
if err = cc.ReadBody(argv.Interface()); err != nil {
return
}
if argIsValue {
@@ -406,16 +339,20 @@ func (server *server) readRequest(codec serverCodec) (service *service, mtype *m
return
}
func (server *server) readRequestHeader(codec serverCodec) (service *service, mtype *methodType, req *request, keepReading bool, err error) {
func (router *router) readHeader(cc codec.Reader) (service *service, mtype *methodType, req *request, keepReading bool, err error) {
// Grab the request header.
req = server.getRequest()
err = codec.ReadRequestHeader(req, true)
msg := new(codec.Message)
msg.Type = codec.Request
req = router.getRequest()
req.msg = msg
err = cc.ReadHeader(msg, msg.Type)
if err != nil {
req = nil
if err == io.EOF || err == io.ErrUnexpectedEOF {
return
}
err = errors.New("rpc: server cannot decode request: " + err.Error())
err = errors.New("rpc: router cannot decode request: " + err.Error())
return
}
@@ -423,30 +360,87 @@ func (server *server) readRequestHeader(codec serverCodec) (service *service, mt
// we can still recover and move on to the next request.
keepReading = true
serviceMethod := strings.Split(req.ServiceMethod, ".")
serviceMethod := strings.Split(req.msg.Endpoint, ".")
if len(serviceMethod) != 2 {
err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod)
err = errors.New("rpc: service/endpoint request ill-formed: " + req.msg.Endpoint)
return
}
// Look up the request.
server.mu.Lock()
service = server.serviceMap[serviceMethod[0]]
server.mu.Unlock()
router.mu.Lock()
service = router.serviceMap[serviceMethod[0]]
router.mu.Unlock()
if service == nil {
err = errors.New("rpc: can't find service " + req.ServiceMethod)
err = errors.New("rpc: can't find service " + serviceMethod[0])
return
}
mtype = service.method[serviceMethod[1]]
if mtype == nil {
err = errors.New("rpc: can't find method " + req.ServiceMethod)
err = errors.New("rpc: can't find method " + serviceMethod[1])
}
return
}
type serverCodec interface {
ReadRequestHeader(*request, bool) error
ReadRequestBody(interface{}) error
WriteResponse(*response, interface{}, bool) error
Close() error
func (router *router) NewHandler(h interface{}, opts ...HandlerOption) Handler {
return newRpcHandler(h, opts...)
}
func (router *router) Handle(h Handler) error {
router.mu.Lock()
defer router.mu.Unlock()
if router.serviceMap == nil {
router.serviceMap = make(map[string]*service)
}
if len(h.Name()) == 0 {
return errors.New("rpc.Handle: handler has no name")
}
if !isExported(h.Name()) {
return errors.New("rpc.Handle: type " + h.Name() + " is not exported")
}
rcvr := h.Handler()
s := new(service)
s.typ = reflect.TypeOf(rcvr)
s.rcvr = reflect.ValueOf(rcvr)
// check name
if _, present := router.serviceMap[h.Name()]; present {
return errors.New("rpc.Handle: service already defined: " + h.Name())
}
s.name = h.Name()
s.method = make(map[string]*methodType)
// Install the methods
for m := 0; m < s.typ.NumMethod(); m++ {
method := s.typ.Method(m)
if mt := prepareMethod(method); mt != nil {
s.method[method.Name] = mt
}
}
// Check there are methods
if len(s.method) == 0 {
return errors.New("rpc Register: type " + s.name + " has no exported methods of suitable type")
}
// save handler
router.serviceMap[s.name] = s
return nil
}
func (router *router) ServeRequest(ctx context.Context, r Request, rsp Response) error {
sending := new(sync.Mutex)
service, mtype, req, argv, replyv, keepReading, err := router.readRequest(r)
if err != nil {
if !keepReading {
return err
}
// send a response if we actually managed to read a header.
if req != nil {
router.freeRequest(req)
}
return err
}
return service.call(ctx, router, sending, mtype, req, argv, replyv, rsp.Codec())
}

View File

@@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/micro/go-log"
log "github.com/micro/go-log"
"github.com/micro/go-micro/broker"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/metadata"
@@ -21,8 +21,8 @@ import (
)
type rpcServer struct {
rpc *server
exit chan chan error
router *router
exit chan chan error
sync.RWMutex
opts Options
@@ -37,19 +37,16 @@ type rpcServer struct {
func newRpcServer(opts ...Option) Server {
options := newOptions(opts...)
return &rpcServer{
opts: options,
rpc: &server{
name: options.Name,
serviceMap: make(map[string]*service),
hdlrWrappers: options.HdlrWrappers,
},
opts: options,
router: DefaultRouter,
handlers: make(map[string]Handler),
subscribers: make(map[*subscriber][]broker.Subscriber),
exit: make(chan chan error),
}
}
func (s *rpcServer) accept(sock transport.Socket) {
// ServeConn serves a single connection
func (s *rpcServer) ServeConn(sock transport.Socket) {
defer func() {
// close socket
sock.Close()
@@ -74,33 +71,17 @@ func (s *rpcServer) accept(sock transport.Socket) {
// we use this Content-Type header to identify the codec needed
ct := msg.Header["Content-Type"]
cf, err := s.newCodec(ct)
// TODO: needs better error handling
if err != nil {
sock.Send(&transport.Message{
Header: map[string]string{
"Content-Type": "text/plain",
},
Body: []byte(err.Error()),
})
s.wg.Done()
return
}
codec := newRpcCodec(&msg, sock, cf)
// strip our headers
hdr := make(map[string]string)
for k, v := range msg.Header {
hdr[k] = v
}
delete(hdr, "Content-Type")
delete(hdr, "Timeout")
// set local/remote ips
hdr["Local"] = sock.Local()
hdr["Remote"] = sock.Remote()
// create new context
ctx := metadata.NewContext(context.Background(), hdr)
// set the timeout if we have it
@@ -110,12 +91,89 @@ func (s *rpcServer) accept(sock transport.Socket) {
}
}
// TODO: needs better error handling
if err := s.rpc.serveRequest(ctx, codec, ct); err != nil {
// no content type
if len(ct) == 0 {
msg.Header["Content-Type"] = DefaultContentType
ct = DefaultContentType
}
// setup old protocol
cf := setupProtocol(&msg)
// no old codec
if cf == nil {
// TODO: needs better error handling
var err error
if cf, err = s.newCodec(ct); err != nil {
sock.Send(&transport.Message{
Header: map[string]string{
"Content-Type": "text/plain",
},
Body: []byte(err.Error()),
})
s.wg.Done()
return
}
}
rcodec := newRpcCodec(&msg, sock, cf)
// internal request
request := &rpcRequest{
service: getHeader("Micro-Service", msg.Header),
method: getHeader("Micro-Method", msg.Header),
endpoint: getHeader("Micro-Endpoint", msg.Header),
contentType: ct,
codec: rcodec,
header: msg.Header,
body: msg.Body,
socket: sock,
stream: true,
}
// internal response
response := &rpcResponse{
header: make(map[string]string),
socket: sock,
codec: rcodec,
}
// set router
r := s.opts.Router
// if nil use default router
if s.opts.Router == nil {
r = s.router
}
// create a wrapped function
handler := func(ctx context.Context, req Request, rsp interface{}) error {
return r.ServeRequest(ctx, req, rsp.(Response))
}
for i := len(s.opts.HdlrWrappers); i > 0; i-- {
handler = s.opts.HdlrWrappers[i-1](handler)
}
// TODO: handle error better
if err := handler(ctx, request, response); err != nil {
if err != lastStreamResponseError {
// write an error response
err = rcodec.Write(&codec.Message{
Header: msg.Header,
Error: err.Error(),
Type: codec.Error,
}, nil)
// could not write the error response
if err != nil {
log.Logf("rpc: unable to write error response: %v", err)
}
}
s.wg.Done()
log.Logf("Unexpected error serving request, closing socket: %v", err)
return
}
// done
s.wg.Done()
}
}
@@ -124,7 +182,7 @@ func (s *rpcServer) newCodec(contentType string) (codec.NewCodec, error) {
if cf, ok := s.opts.Codecs[contentType]; ok {
return cf, nil
}
if cf, ok := defaultCodecs[contentType]; ok {
if cf, ok := DefaultCodecs[contentType]; ok {
return cf, nil
}
return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
@@ -142,25 +200,19 @@ func (s *rpcServer) Init(opts ...Option) error {
for _, opt := range opts {
opt(&s.opts)
}
// update internal server
s.rpc = &server{
name: s.opts.Name,
serviceMap: s.rpc.serviceMap,
hdlrWrappers: s.opts.HdlrWrappers,
}
s.Unlock()
return nil
}
func (s *rpcServer) NewHandler(h interface{}, opts ...HandlerOption) Handler {
return newRpcHandler(h, opts...)
return s.router.NewHandler(h, opts...)
}
func (s *rpcServer) Handle(h Handler) error {
s.Lock()
defer s.Unlock()
if err := s.rpc.register(h.Handler()); err != nil {
if err := s.router.Handle(h); err != nil {
return err
}
@@ -224,18 +276,25 @@ func (s *rpcServer) Register() error {
return err
}
// make copy of metadata
md := make(metadata.Metadata)
for k, v := range config.Metadata {
md[k] = v
}
// register service
node := &registry.Node{
Id: config.Name + "-" + config.Id,
Address: addr,
Port: port,
Metadata: config.Metadata,
Metadata: md,
}
node.Metadata["transport"] = config.Transport.String()
node.Metadata["broker"] = config.Broker.String()
node.Metadata["server"] = s.String()
node.Metadata["registry"] = config.Registry.String()
node.Metadata["protocol"] = "mucp"
s.RLock()
// Maps are ordered randomly, sort the keys for consistency
@@ -306,6 +365,9 @@ func (s *rpcServer) Register() error {
if queue := sb.Options().Queue; len(queue) > 0 {
opts = append(opts, broker.Queue(queue))
}
if cx := sb.Options().Context; cx != nil {
opts = append(opts, broker.SubscribeContext(cx))
}
sub, err := config.Broker.Subscribe(sb.Topic(), handler, opts...)
if err != nil {
return err
@@ -385,36 +447,53 @@ func (s *rpcServer) Start() error {
registerDebugHandler(s)
config := s.Options()
// start listening on the transport
ts, err := config.Transport.Listen(config.Address)
if err != nil {
return err
}
log.Logf("Listening on %s", ts.Addr())
s.Lock()
log.Logf("Transport [%s] Listening on %s", config.Transport.String(), ts.Addr())
// swap address
s.Lock()
addr := s.opts.Address
s.opts.Address = ts.Addr()
s.Unlock()
exit := make(chan bool, 1)
// connect to the broker
if err := config.Broker.Connect(); err != nil {
return err
}
log.Logf("Broker [%s] Listening on %s", config.Broker.String(), config.Broker.Address())
// announce self to the world
if err := s.Register(); err != nil {
log.Log("Server register error: ", err)
}
exit := make(chan bool)
go func() {
for {
err := ts.Accept(s.accept)
// listen for connections
err := ts.Accept(s.ServeConn)
// TODO: listen for messages
// msg := broker.Exchange(service).Consume()
// check if we're supposed to exit
select {
// check if we're supposed to exit
case <-exit:
return
default:
}
// check the error and backoff
if err != nil {
log.Logf("Accept error: %v", err)
time.Sleep(time.Second)
continue
default:
if err != nil {
log.Logf("Accept error: %v", err)
time.Sleep(time.Second)
continue
}
}
// no error just exit
@@ -423,9 +502,37 @@ func (s *rpcServer) Start() error {
}()
go func() {
// wait for exit
ch := <-s.exit
exit <- true
t := new(time.Ticker)
// only process if it exists
if s.opts.RegisterInterval > time.Duration(0) {
// new ticker
t = time.NewTicker(s.opts.RegisterInterval)
}
// return error chan
var ch chan error
Loop:
for {
select {
// register self on interval
case <-t.C:
if err := s.Register(); err != nil {
log.Log("Server register error: ", err)
}
// wait for exit
case ch = <-s.exit:
t.Stop()
close(exit)
break Loop
}
}
// deregister self
if err := s.Deregister(); err != nil {
log.Log("Server deregister error: ", err)
}
// wait for requests to finish
if wait(s.opts.Context) {
@@ -438,14 +545,13 @@ func (s *rpcServer) Start() error {
// disconnect the broker
config.Broker.Disconnect()
s.Lock()
// swap back address
s.Lock()
s.opts.Address = addr
s.Unlock()
}()
// TODO: subscribe to cruft
return config.Broker.Connect()
return nil
}
func (s *rpcServer) Stop() error {

View File

@@ -3,16 +3,18 @@ package server
import (
"context"
"sync"
"github.com/micro/go-micro/codec"
)
// Implements the Streamer interface
type rpcStream struct {
sync.RWMutex
seq uint64
id string
closed bool
err error
request Request
codec serverCodec
codec codec.Codec
context context.Context
}
@@ -28,29 +30,33 @@ func (r *rpcStream) Send(msg interface{}) error {
r.Lock()
defer r.Unlock()
resp := response{
ServiceMethod: r.request.Method(),
Seq: r.seq,
resp := codec.Message{
Target: r.request.Service(),
Method: r.request.Method(),
Endpoint: r.request.Endpoint(),
Id: r.id,
Type: codec.Response,
}
return r.codec.WriteResponse(&resp, msg, false)
return r.codec.Write(&resp, msg)
}
func (r *rpcStream) Recv(msg interface{}) error {
r.Lock()
defer r.Unlock()
req := request{}
req := new(codec.Message)
req.Type = codec.Request
if err := r.codec.ReadRequestHeader(&req, false); err != nil {
if err := r.codec.ReadHeader(req, req.Type); err != nil {
// discard body
r.codec.ReadRequestBody(nil)
r.codec.ReadBody(nil)
return err
}
// we need to stay up to date with sequence numbers
r.seq = req.Seq
return r.codec.ReadRequestBody(msg)
r.id = req.Id
return r.codec.ReadBody(msg)
}
func (r *rpcStream) Error() error {

View File

@@ -9,6 +9,7 @@ import (
"github.com/google/uuid"
"github.com/micro/go-log"
"github.com/micro/go-micro/codec"
"github.com/micro/go-micro/registry"
)
@@ -20,13 +21,17 @@ type Server interface {
NewHandler(interface{}, ...HandlerOption) Handler
NewSubscriber(string, interface{}, ...SubscriberOption) Subscriber
Subscribe(Subscriber) error
Register() error
Deregister() error
Start() error
Stop() error
String() string
}
// Router handle serving messages
type Router interface {
// ServeRequest processes a request to completion
ServeRequest(context.Context, Request, Response) error
}
// Message is an async message interface
type Message interface {
Topic() string
@@ -36,14 +41,36 @@ type Message interface {
// Request is a synchronous request interface
type Request interface {
// Service name requested
Service() string
// The action requested
Method() string
// Endpoint name requested
Endpoint() string
// Content type provided
ContentType() string
Request() interface{}
// indicates whether the request will be streamed
// Header of the request
Header() map[string]string
// Body is the initial decoded value
Body() interface{}
// Read the undecoded request body
Read() ([]byte, error)
// The encoded message stream
Codec() codec.Reader
// Indicates whether its a stream
Stream() bool
}
// Response is the response writer for unencoded messages
type Response interface {
// Encoded writer
Codec() codec.Writer
// Write the header
WriteHeader(map[string]string)
// write a response directly to the client
Write([]byte) error
}
// Stream represents a stream established with a client.
// A stream can be bidirectional which is indicated by the request.
// The last error will be left in Error().
@@ -58,7 +85,7 @@ type Stream interface {
}
// Handler interface represents a request handler. It's generated
// by passing any type of public concrete object with methods into server.NewHandler.
// by passing any type of public concrete object with endpoints into server.NewHandler.
// Most will pass in a struct.
//
// Example:
@@ -77,7 +104,7 @@ type Handler interface {
}
// Subscriber interface represents a subscription to a given topic using
// a specific subscriber function or object with methods.
// a specific subscriber function or object with endpoints.
type Subscriber interface {
Topic() string
Subscriber() interface{}
@@ -87,16 +114,13 @@ type Subscriber interface {
type Option func(*Options)
type HandlerOption func(*HandlerOptions)
type SubscriberOption func(*SubscriberOptions)
var (
DefaultAddress = ":0"
DefaultName = "go-server"
DefaultVersion = "1.0.0"
DefaultId = uuid.New().String()
DefaultServer Server = newRpcServer()
DefaultRouter = newRpcRouter()
)
// DefaultOptions returns config options for the default service
@@ -125,7 +149,7 @@ func NewSubscriber(topic string, h interface{}, opts ...SubscriberOption) Subscr
// NewHandler creates a new handler interface using the default server
// Handlers are required to be a public object with public
// methods. Call to a service method such as Foo.Bar expects
// endpoints. Call to a service endpoint such as Foo.Bar expects
// the type:
//
// type Foo struct {}
@@ -149,16 +173,6 @@ func Subscribe(s Subscriber) error {
return DefaultServer.Subscribe(s)
}
// Register registers the default server with the discovery system
func Register() error {
return DefaultServer.Register()
}
// Deregister deregisters the default server from the discovery system
func Deregister() error {
return DefaultServer.Deregister()
}
// Run starts the default server and waits for a kill
// signal before exiting. Also registers/deregisters the server
func Run() error {
@@ -166,18 +180,10 @@ func Run() error {
return err
}
if err := DefaultServer.Register(); err != nil {
return err
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)
log.Logf("Received signal %s", <-ch)
if err := DefaultServer.Deregister(); err != nil {
return err
}
return Stop()
}

View File

@@ -164,17 +164,29 @@ func validateSubscriber(sub Subscriber) error {
func (s *rpcServer) createSubHandler(sb *subscriber, opts Options) broker.Handler {
return func(p broker.Publication) error {
msg := p.Message()
// get codec
ct := msg.Header["Content-Type"]
// default content type
if len(ct) == 0 {
msg.Header["Content-Type"] = DefaultContentType
ct = DefaultContentType
}
// get codec
cf, err := s.newCodec(ct)
if err != nil {
return err
}
// copy headers
hdr := make(map[string]string)
for k, v := range msg.Header {
hdr[k] = v
}
delete(hdr, "Content-Type")
// create context
ctx := metadata.NewContext(context.Background(), hdr)
results := make(chan error, len(sb.handlers))

View File

@@ -5,10 +5,7 @@ import (
"os/signal"
"sync"
"syscall"
"time"
"github.com/micro/cli"
"github.com/micro/go-log"
"github.com/micro/go-micro/client"
"github.com/micro/go-micro/cmd"
"github.com/micro/go-micro/metadata"
@@ -36,27 +33,6 @@ func newService(opts ...Option) Service {
}
}
func (s *service) run(exit chan bool) {
if s.opts.RegisterInterval <= time.Duration(0) {
return
}
t := time.NewTicker(s.opts.RegisterInterval)
for {
select {
case <-t.C:
err := s.opts.Server.Register()
if err != nil {
log.Log("service run Server.Register error: ", err)
}
case <-exit:
t.Stop()
return
}
}
}
// Init initialises options. Additionally it calls cmd.Init
// which parses command line flags. cmd.Init is only called
// on first Init.
@@ -67,20 +43,6 @@ func (s *service) Init(opts ...Option) {
}
s.once.Do(func() {
// save user action
action := s.opts.Cmd.App().Action
// set service action
s.opts.Cmd.App().Action = func(c *cli.Context) {
// set register interval
if i := time.Duration(c.GlobalInt("register_interval")); i > 0 {
s.opts.RegisterInterval = i * time.Second
}
// user action
action(c)
}
// Initialise the command flags, overriding new service
_ = s.opts.Cmd.Init(
cmd.Broker(&s.opts.Broker),
@@ -105,7 +67,7 @@ func (s *service) Server() server.Server {
}
func (s *service) String() string {
return "go-micro"
return "micro"
}
func (s *service) Start() error {
@@ -119,10 +81,6 @@ func (s *service) Start() error {
return err
}
if err := s.opts.Server.Register(); err != nil {
return err
}
for _, fn := range s.opts.AfterStart {
if err := fn(); err != nil {
return err
@@ -141,10 +99,6 @@ func (s *service) Stop() error {
}
}
if err := s.opts.Server.Deregister(); err != nil {
return err
}
if err := s.opts.Server.Stop(); err != nil {
return err
}
@@ -163,10 +117,6 @@ func (s *service) Run() error {
return err
}
// start reg loop
ex := make(chan bool)
go s.run(ex)
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
@@ -177,8 +127,5 @@ func (s *service) Run() error {
case <-s.opts.Context.Done():
}
// exit reg loop
close(ex)
return s.Stop()
}

View File

@@ -2,62 +2,179 @@ package micro
import (
"context"
"errors"
"sync"
"testing"
"github.com/micro/go-micro/registry/mock"
glog "github.com/go-log/log"
"github.com/micro/go-log"
"github.com/micro/go-micro/client"
"github.com/micro/go-micro/registry/memory"
proto "github.com/micro/go-micro/server/debug/proto"
)
func TestService(t *testing.T) {
var wg sync.WaitGroup
func testShutdown(wg *sync.WaitGroup, cancel func()) {
// add 1
wg.Add(1)
// shutdown the service
cancel()
// wait for stop
wg.Wait()
}
func testService(ctx context.Context, wg *sync.WaitGroup, name string) Service {
// set no op logger
log.SetLogger(glog.DefaultLogger)
// add self
wg.Add(1)
// cancellation context
ctx, cancel := context.WithCancel(context.Background())
r := memory.NewRegistry()
r.(*memory.Registry).Setup()
// create service
service := NewService(
Name("test.service"),
return NewService(
Name(name),
Context(ctx),
Registry(mock.NewRegistry()),
Registry(r),
AfterStart(func() error {
wg.Done()
return nil
}),
AfterStop(func() error {
wg.Done()
return nil
}),
)
}
func testRequest(ctx context.Context, c client.Client, name string) error {
// test call debug
req := c.NewRequest(
name,
"Debug.Health",
new(proto.HealthRequest),
)
// we can't test service.Init as it parses the command line
// service.Init()
rsp := new(proto.HealthResponse)
err := c.Call(context.TODO(), req, rsp)
if err != nil {
return err
}
if rsp.Status != "ok" {
return errors.New("service response: " + rsp.Status)
}
return nil
}
// TestService tests running and calling a service
func TestService(t *testing.T) {
// waitgroup for server start
var wg sync.WaitGroup
// cancellation context
ctx, cancel := context.WithCancel(context.Background())
// start test server
service := testService(ctx, &wg, "test.service")
// run service
go func() {
// wait for start
// wait for service to start
wg.Wait()
// test call debug
req := service.Client().NewRequest(
"test.service",
"Debug.Health",
new(proto.HealthRequest),
)
rsp := new(proto.HealthResponse)
err := service.Client().Call(context.TODO(), req, rsp)
if err != nil {
// make a test call
if err := testRequest(ctx, service.Client(), "test.service"); err != nil {
t.Fatal(err)
}
if rsp.Status != "ok" {
t.Fatalf("service response: %s", rsp.Status)
}
// shutdown the service
cancel()
testShutdown(&wg, cancel)
}()
// start service
if err := service.Run(); err != nil {
t.Fatal(err)
}
}
func benchmarkService(b *testing.B, n int, name string) {
// stop the timer
b.StopTimer()
// waitgroup for server start
var wg sync.WaitGroup
// cancellation context
ctx, cancel := context.WithCancel(context.Background())
// create test server
service := testService(ctx, &wg, name)
// start the server
go func() {
if err := service.Run(); err != nil {
b.Fatal(err)
}
}()
// wait for service to start
wg.Wait()
// make a test call to warm the cache
for i := 0; i < 10; i++ {
if err := testRequest(ctx, service.Client(), name); err != nil {
b.Fatal(err)
}
}
// start the timer
b.StartTimer()
// number of iterations
for i := 0; i < b.N; i++ {
// for concurrency
for j := 0; j < n; j++ {
wg.Add(1)
go func() {
err := testRequest(ctx, service.Client(), name)
wg.Done()
if err != nil {
b.Fatal(err)
}
}()
}
// wait for test completion
wg.Wait()
}
// stop the timer
b.StopTimer()
// shutdown service
testShutdown(&wg, cancel)
}
func BenchmarkService1(b *testing.B) {
benchmarkService(b, 1, "test.service.1")
}
func BenchmarkService8(b *testing.B) {
benchmarkService(b, 8, "test.service.8")
}
func BenchmarkService16(b *testing.B) {
benchmarkService(b, 16, "test.service.16")
}
func BenchmarkService32(b *testing.B) {
benchmarkService(b, 32, "test.service.32")
}
func BenchmarkService64(b *testing.B) {
benchmarkService(b, 64, "test.service.64")
}

View File

@@ -1,10 +0,0 @@
package codec
// Codec is used for encoding where the transport doesn't natively support
// headers in the message type. In this case the entire message is
// encoded as the payload
type Codec interface {
Marshal(interface{}) ([]byte, error)
Unmarshal([]byte, interface{}) error
String() string
}

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