diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..b6392e26 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +issuehunt: micro/development diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..1899438a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: For reporting bugs in go-micro +title: "[BUG]" +labels: '' +assignees: '' + +--- + +**Describe the bug** + +1. What are you trying to do? +2. What did you expect to happen? +3. What happens instead? + +**How to reproduce the bug:** + +If possible, please include a minimal code snippet here. + +**Environment:** +Go Version: please paste `go version` output here +``` +please paste `go env` output here +``` diff --git a/.github/ISSUE_TEMPLATE/feature-request---enhancement.md b/.github/ISSUE_TEMPLATE/feature-request---enhancement.md new file mode 100644 index 00000000..459817f4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request---enhancement.md @@ -0,0 +1,17 @@ +--- +name: Feature request / Enhancement +about: If you have a need not served by go-micro +title: "[FEATURE]" +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..1daf48b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,14 @@ +--- +name: Question +about: Ask a question about go-micro +title: '' +labels: '' +assignees: '' + +--- + +Before asking, please check if your question has already been answered: + +1. Check the documentation - https://micro.mu/docs/ +2. Check the examples and plugins - https://github.com/micro/examples & https://github.com/micro/go-plugins +3. Search existing issues diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..0228bfc5 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,26 @@ +run: + deadline: 10m +linters: + disable-all: false + enable-all: false + enable: + - megacheck + - staticcheck + - deadcode + - varcheck + - gosimple + - unused + - prealloc + - scopelint + - gocritic + - goimports + - unconvert + - govet + - nakedret + - structcheck + - gosec + disable: + - maligned + - interfacer + - typecheck + - dupl diff --git a/.travis.yml b/.travis.yml index 211cb587..d26a9c52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,14 @@ language: go go: -- 1.12.x +- 1.13.x env: - - GO111MODULE=on + - GO111MODULE=on IN_TRAVIS_CI=yes +before_script: + - go install github.com/golangci/golangci-lint/cmd/golangci-lint +script: + - golangci-lint run || true + - go test -v -race ./... || true + - go test -v ./... notifications: slack: secure: aEvhLbhujaGaKSrOokiG3//PaVHTIrc3fBpoRbCRqfZpyq6WREoapJJhF+tIpWWOwaC9GmChbD6aHo/jMUgwKXVyPSaNjiEL87YzUUpL8B2zslNp1rgfTg/LrzthOx3Q1TYwpaAl3to0fuHUVFX4yMeC2vuThq7WSXgMMxFCtbc= diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..0be20e66 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +go-micro.dev \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4d993447 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.13-alpine + +RUN mkdir /user && \ + echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \ + echo 'nobody:x:65534:' > /user/group + +ENV GO111MODULE=on +RUN apk --no-cache add make git gcc libtool musl-dev ca-certificates && \ + rm -rf /var/cache/apk/* /tmp/* + +WORKDIR / +COPY ./go.mod ./go.sum ./ +RUN go mod download && rm go.mod go.sum diff --git a/README.md b/README.md index 1c689389..02763aa7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ but everything can be easily swapped out. Plugins are available at [github.com/micro/go-plugins](https://github.com/micro/go-plugins). -Follow us on [Twitter](https://twitter.com/microhq) or join the [Slack](http://slack.micro.mu/) community. +Follow us on [Twitter](https://twitter.com/microhq) or join the [Slack](https://micro.mu/slack) community. ## Features @@ -20,8 +20,7 @@ Go Micro abstracts away the details of distributed systems. Here are the main fe - **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. 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. +multicast DNS (mdns), a zeroconf system. - **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 @@ -45,5 +44,5 @@ are pluggable and allows Go Micro to be runtime agnostic. You can plugin any und ## Getting Started -See the [docs](https://micro.mu/docs/go-micro.html) for detailed information on the architecture, installation and use of go-micro. +See the [docs](https://micro.mu/docs/framework.html) for detailed information on the architecture, installation and use of go-micro. diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..3397c9a4 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-architect \ No newline at end of file diff --git a/agent/input/telegram/conn.go b/agent/input/telegram/conn.go index 44d1ada1..78a8ff3a 100644 --- a/agent/input/telegram/conn.go +++ b/agent/input/telegram/conn.go @@ -8,7 +8,7 @@ import ( "github.com/forestgiant/sliceutil" "github.com/micro/go-micro/agent/input" "github.com/micro/go-micro/util/log" - "gopkg.in/telegram-bot-api.v4" + tgbotapi "gopkg.in/telegram-bot-api.v4" ) type telegramConn struct { @@ -44,11 +44,9 @@ func (tc *telegramConn) run() { tc.recv = updates tc.syncCond.Signal() - for { - select { - case <-tc.exit: - return - } + select { + case <-tc.exit: + return } } diff --git a/agent/input/telegram/telegram.go b/agent/input/telegram/telegram.go index 87566e4e..a1510527 100644 --- a/agent/input/telegram/telegram.go +++ b/agent/input/telegram/telegram.go @@ -7,7 +7,7 @@ import ( "github.com/micro/cli" "github.com/micro/go-micro/agent/input" - "gopkg.in/telegram-bot-api.v4" + tgbotapi "gopkg.in/telegram-bot-api.v4" ) type telegramInput struct { diff --git a/agent/proto/bot.pb.go b/agent/proto/bot.pb.go index f36d9f4a..b51da305 100644 --- a/agent/proto/bot.pb.go +++ b/agent/proto/bot.pb.go @@ -1,11 +1,13 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: github.com/micro/go-bot/proto/bot.proto +// source: github.com/micro/go-micro/agent/proto/bot.proto package go_micro_bot -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal @@ -16,7 +18,7 @@ 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.ProtoPackageIsVersion2 // please upgrade the proto package +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type HelpRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -28,16 +30,17 @@ func (m *HelpRequest) Reset() { *m = HelpRequest{} } func (m *HelpRequest) String() string { return proto.CompactTextString(m) } func (*HelpRequest) ProtoMessage() {} func (*HelpRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_bot_654832eab83ed4b5, []int{0} + return fileDescriptor_018e8d5b14a89d12, []int{0} } + func (m *HelpRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_HelpRequest.Unmarshal(m, b) } func (m *HelpRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_HelpRequest.Marshal(b, m, deterministic) } -func (dst *HelpRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_HelpRequest.Merge(dst, src) +func (m *HelpRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_HelpRequest.Merge(m, src) } func (m *HelpRequest) XXX_Size() int { return xxx_messageInfo_HelpRequest.Size(m) @@ -60,16 +63,17 @@ func (m *HelpResponse) Reset() { *m = HelpResponse{} } func (m *HelpResponse) String() string { return proto.CompactTextString(m) } func (*HelpResponse) ProtoMessage() {} func (*HelpResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_bot_654832eab83ed4b5, []int{1} + return fileDescriptor_018e8d5b14a89d12, []int{1} } + func (m *HelpResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_HelpResponse.Unmarshal(m, b) } func (m *HelpResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_HelpResponse.Marshal(b, m, deterministic) } -func (dst *HelpResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_HelpResponse.Merge(dst, src) +func (m *HelpResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_HelpResponse.Merge(m, src) } func (m *HelpResponse) XXX_Size() int { return xxx_messageInfo_HelpResponse.Size(m) @@ -105,16 +109,17 @@ func (m *ExecRequest) Reset() { *m = ExecRequest{} } func (m *ExecRequest) String() string { return proto.CompactTextString(m) } func (*ExecRequest) ProtoMessage() {} func (*ExecRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_bot_654832eab83ed4b5, []int{2} + return fileDescriptor_018e8d5b14a89d12, []int{2} } + func (m *ExecRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ExecRequest.Unmarshal(m, b) } func (m *ExecRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_ExecRequest.Marshal(b, m, deterministic) } -func (dst *ExecRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExecRequest.Merge(dst, src) +func (m *ExecRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ExecRequest.Merge(m, src) } func (m *ExecRequest) XXX_Size() int { return xxx_messageInfo_ExecRequest.Size(m) @@ -144,16 +149,17 @@ func (m *ExecResponse) Reset() { *m = ExecResponse{} } func (m *ExecResponse) String() string { return proto.CompactTextString(m) } func (*ExecResponse) ProtoMessage() {} func (*ExecResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_bot_654832eab83ed4b5, []int{3} + return fileDescriptor_018e8d5b14a89d12, []int{3} } + func (m *ExecResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ExecResponse.Unmarshal(m, b) } func (m *ExecResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_ExecResponse.Marshal(b, m, deterministic) } -func (dst *ExecResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_ExecResponse.Merge(dst, src) +func (m *ExecResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ExecResponse.Merge(m, src) } func (m *ExecResponse) XXX_Size() int { return xxx_messageInfo_ExecResponse.Size(m) @@ -186,25 +192,25 @@ func init() { } func init() { - proto.RegisterFile("github.com/micro/go-bot/proto/bot.proto", fileDescriptor_bot_654832eab83ed4b5) + proto.RegisterFile("github.com/micro/go-micro/agent/proto/bot.proto", fileDescriptor_018e8d5b14a89d12) } -var fileDescriptor_bot_654832eab83ed4b5 = []byte{ +var fileDescriptor_018e8d5b14a89d12 = []byte{ // 246 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x41, 0x4b, 0xc4, 0x30, - 0x10, 0x85, 0xb7, 0xba, 0xae, 0xec, 0xb4, 0x5e, 0x82, 0x48, 0xdd, 0x53, 0xcd, 0xc5, 0xbd, 0x98, - 0x82, 0x5e, 0x05, 0x0f, 0xa2, 0x78, 0xee, 0x3f, 0x68, 0xba, 0x43, 0x2c, 0x6c, 0x3b, 0x35, 0x99, - 0x82, 0xff, 0xc1, 0x3f, 0x2d, 0x4d, 0x72, 0x08, 0xcb, 0xde, 0xe6, 0x65, 0x86, 0xf7, 0xbe, 0x17, - 0x78, 0x34, 0x3d, 0x7f, 0xcf, 0x5a, 0x75, 0x34, 0xd4, 0x43, 0xdf, 0x59, 0xaa, 0x0d, 0x3d, 0x69, - 0xe2, 0x7a, 0xb2, 0xc4, 0x54, 0x6b, 0x62, 0xe5, 0x27, 0x51, 0x18, 0x52, 0xfe, 0x40, 0x69, 0x62, - 0x79, 0x03, 0xf9, 0x17, 0x1e, 0xa7, 0x06, 0x7f, 0x66, 0x74, 0x2c, 0x3f, 0xa1, 0x08, 0xd2, 0x4d, - 0x34, 0x3a, 0x14, 0xb7, 0x70, 0x35, 0xbb, 0xd6, 0x60, 0x99, 0x55, 0xd9, 0x7e, 0xdb, 0x04, 0x21, - 0x2a, 0xc8, 0x0f, 0xe8, 0x3a, 0xdb, 0x4f, 0xdc, 0xd3, 0x58, 0x5e, 0xf8, 0x5d, 0xfa, 0x24, 0x1f, - 0x20, 0xff, 0xf8, 0xc5, 0x2e, 0xda, 0x0a, 0x01, 0xeb, 0xd6, 0x1a, 0x57, 0x66, 0xd5, 0xe5, 0x7e, - 0xdb, 0xf8, 0x59, 0xbe, 0x42, 0x11, 0x4e, 0x62, 0xd4, 0x1d, 0x6c, 0x2c, 0xba, 0xf9, 0xc8, 0x3e, - 0xab, 0x68, 0xa2, 0x5a, 0x10, 0xd0, 0x5a, 0xb2, 0x31, 0x26, 0x88, 0xe7, 0xbf, 0x0c, 0xae, 0xdf, - 0x69, 0x18, 0xda, 0xf1, 0x20, 0xde, 0x60, 0xbd, 0x40, 0x8b, 0x7b, 0x95, 0x56, 0x53, 0x49, 0xaf, - 0xdd, 0xee, 0xdc, 0x2a, 0x04, 0xcb, 0xd5, 0x62, 0xb0, 0xa0, 0x9c, 0x1a, 0x24, 0x0d, 0x4e, 0x0d, - 0x52, 0x72, 0xb9, 0xd2, 0x1b, 0xff, 0xb5, 0x2f, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0xcb, 0x77, - 0xdf, 0x28, 0x85, 0x01, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x50, 0x4d, 0x4b, 0xc4, 0x30, + 0x10, 0xdd, 0xea, 0xba, 0xb2, 0xd3, 0x7a, 0x09, 0x22, 0x75, 0x4f, 0x35, 0xa7, 0xbd, 0x98, 0x80, + 0x5e, 0x05, 0x0f, 0xa2, 0x78, 0xee, 0x3f, 0x68, 0xbb, 0x43, 0x2c, 0x6c, 0x3b, 0x35, 0x99, 0x82, + 0xff, 0xc1, 0x3f, 0x2d, 0x4d, 0x72, 0x08, 0xc5, 0xdb, 0x7b, 0x79, 0xe1, 0x7d, 0x0c, 0x68, 0xd3, + 0xf3, 0xd7, 0xdc, 0xaa, 0x8e, 0x06, 0x3d, 0xf4, 0x9d, 0x25, 0x6d, 0xe8, 0x31, 0x80, 0xc6, 0xe0, + 0xc8, 0x7a, 0xb2, 0xc4, 0xa4, 0x5b, 0x62, 0xe5, 0x91, 0x28, 0x0c, 0x29, 0xaf, 0xab, 0x96, 0x58, + 0xde, 0x40, 0xfe, 0x89, 0xe7, 0xa9, 0xc6, 0xef, 0x19, 0x1d, 0xcb, 0x0f, 0x28, 0x02, 0x75, 0x13, + 0x8d, 0x0e, 0xc5, 0x2d, 0x5c, 0xcd, 0xae, 0x31, 0x58, 0x66, 0x55, 0x76, 0xdc, 0xd7, 0x81, 0x88, + 0x0a, 0xf2, 0x13, 0xba, 0xce, 0xf6, 0x13, 0xf7, 0x34, 0x96, 0x17, 0x5e, 0x4b, 0x9f, 0xe4, 0x03, + 0xe4, 0xef, 0x3f, 0xd8, 0x45, 0x5b, 0x21, 0x60, 0xdb, 0x58, 0xe3, 0xca, 0xac, 0xba, 0x3c, 0xee, + 0x6b, 0x8f, 0xe5, 0x0b, 0x14, 0xe1, 0x4b, 0x8c, 0xba, 0x83, 0x9d, 0x45, 0x37, 0x9f, 0xd9, 0x67, + 0x15, 0x75, 0x64, 0x4b, 0x05, 0xb4, 0x96, 0x6c, 0x8c, 0x09, 0xe4, 0xe9, 0x37, 0x83, 0xeb, 0x37, + 0x1a, 0x86, 0x66, 0x3c, 0x89, 0x57, 0xd8, 0x2e, 0xa5, 0xc5, 0xbd, 0x4a, 0xa7, 0xa9, 0x64, 0xd7, + 0xe1, 0xf0, 0x9f, 0x14, 0x82, 0xe5, 0x66, 0x31, 0x58, 0xaa, 0xac, 0x0d, 0x92, 0x05, 0x6b, 0x83, + 0xb4, 0xb9, 0xdc, 0xb4, 0x3b, 0x7f, 0xda, 0xe7, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x18, 0xbd, + 0x39, 0x29, 0x8d, 0x01, 0x00, 0x00, } diff --git a/agent/proto/bot.micro.go b/agent/proto/bot.pb.micro.go similarity index 85% rename from agent/proto/bot.micro.go rename to agent/proto/bot.pb.micro.go index 0a027a82..1c04fe59 100644 --- a/agent/proto/bot.micro.go +++ b/agent/proto/bot.pb.micro.go @@ -1,23 +1,13 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: github.com/micro/go-bot/proto/bot.proto +// source: github.com/micro/go-micro/agent/proto/bot.proto -/* -Package go_micro_bot is a generated protocol buffer package. - -It is generated from these files: - github.com/micro/go-bot/proto/bot.proto - -It has these top-level messages: - HelpRequest - HelpResponse - ExecRequest - ExecResponse -*/ package go_micro_bot -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) import ( context "context" @@ -34,7 +24,7 @@ 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.ProtoPackageIsVersion2 // please upgrade the proto package +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package // Reference imports to suppress errors if they are not otherwise used. var _ context.Context @@ -94,12 +84,12 @@ type CommandHandler interface { } func RegisterCommandHandler(s server.Server, hdlr CommandHandler, opts ...server.HandlerOption) error { - type _command interface { + type command interface { Help(ctx context.Context, in *HelpRequest, out *HelpResponse) error Exec(ctx context.Context, in *ExecRequest, out *ExecResponse) error } type Command struct { - _command + command } h := &commandHandler{hdlr} return s.Handle(s.NewHandler(&Command{h}, opts...)) diff --git a/api/handler/api/util.go b/api/handler/api/util.go index 7a4607c7..824ec5da 100644 --- a/api/handler/api/util.go +++ b/api/handler/api/util.go @@ -29,16 +29,20 @@ func requestToProto(r *http.Request) (*api.Request, error) { ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { - ct = "application/x-www-form-urlencoded" + ct = "text/plain; charset=UTF-8" //default CT is text/plain r.Header.Set("Content-Type", ct) } - switch ct { - case "application/x-www-form-urlencoded": - // expect form vals - default: - data, _ := ioutil.ReadAll(r.Body) - req.Body = string(data) + //set the body: + if r.Body != nil { + switch ct { + case "application/x-www-form-urlencoded": + // expect form vals in Post data + default: + + data, _ := ioutil.ReadAll(r.Body) + req.Body = string(data) + } } // Set X-Forwarded-For if it does not exist diff --git a/api/handler/api/util_test.go b/api/handler/api/util_test.go index 0a82a03d..9350bcde 100644 --- a/api/handler/api/util_test.go +++ b/api/handler/api/util_test.go @@ -8,7 +8,7 @@ import ( func TestRequestToProto(t *testing.T) { testData := []*http.Request{ - &http.Request{ + { Method: "GET", Header: http.Header{ "Header": []string{"test"}, diff --git a/api/handler/cloudevents/event.go b/api/handler/cloudevents/event.go index 6c0c6cf9..4869188b 100644 --- a/api/handler/cloudevents/event.go +++ b/api/handler/cloudevents/event.go @@ -32,7 +32,7 @@ import ( "unicode" "github.com/google/uuid" - "gopkg.in/go-playground/validator.v9" + validator "gopkg.in/go-playground/validator.v9" ) const ( diff --git a/api/handler/event/event.go b/api/handler/event/event.go index 6fb45ae8..8eb65aa1 100644 --- a/api/handler/event/event.go +++ b/api/handler/event/event.go @@ -2,6 +2,7 @@ package event import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -91,12 +92,17 @@ func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // set body - b, err := ioutil.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), 500) - return + if r.Method == "GET" { + bytes, _ := json.Marshal(r.URL.Query()) + ev.Data = string(bytes) + } else { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + ev.Data = string(b) } - ev.Data = string(b) // get client c := e.options.Service.Client() diff --git a/api/handler/http/http_test.go b/api/handler/http/http_test.go index 31847221..9812535c 100644 --- a/api/handler/http/http_test.go +++ b/api/handler/http/http_test.go @@ -27,7 +27,7 @@ func testHttp(t *testing.T, path, service, ns string) { s := ®istry.Service{ Name: service, Nodes: []*registry.Node{ - ®istry.Node{ + { Id: service + "-1", Address: l.Addr().String(), }, diff --git a/api/handler/rpc/rpc_test.go b/api/handler/rpc/rpc_test.go index 2804a84c..f50cdedf 100644 --- a/api/handler/rpc/rpc_test.go +++ b/api/handler/rpc/rpc_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/golang/protobuf/proto" - "github.com/micro/go-micro/api/proto" + go_api "github.com/micro/go-micro/api/proto" ) func TestRequestPayloadFromRequest(t *testing.T) { diff --git a/api/proto/api.pb.go b/api/proto/api.pb.go index 3304d4e1..9e912f49 100644 --- a/api/proto/api.pb.go +++ b/api/proto/api.pb.go @@ -3,9 +3,11 @@ package go_api -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal @@ -16,7 +18,7 @@ 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.ProtoPackageIsVersion2 // please upgrade the proto package +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type Pair struct { Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -30,16 +32,17 @@ func (m *Pair) Reset() { *m = Pair{} } func (m *Pair) String() string { return proto.CompactTextString(m) } func (*Pair) ProtoMessage() {} func (*Pair) Descriptor() ([]byte, []int) { - return fileDescriptor_api_17a7876430d97ebd, []int{0} + return fileDescriptor_7b6696ef87ec1943, []int{0} } + func (m *Pair) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Pair.Unmarshal(m, b) } func (m *Pair) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Pair.Marshal(b, m, deterministic) } -func (dst *Pair) XXX_Merge(src proto.Message) { - xxx_messageInfo_Pair.Merge(dst, src) +func (m *Pair) XXX_Merge(src proto.Message) { + xxx_messageInfo_Pair.Merge(m, src) } func (m *Pair) XXX_Size() int { return xxx_messageInfo_Pair.Size(m) @@ -83,16 +86,17 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_api_17a7876430d97ebd, []int{1} + return fileDescriptor_7b6696ef87ec1943, []int{1} } + func (m *Request) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Request.Unmarshal(m, b) } func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Request.Marshal(b, m, deterministic) } -func (dst *Request) XXX_Merge(src proto.Message) { - xxx_messageInfo_Request.Merge(dst, src) +func (m *Request) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request.Merge(m, src) } func (m *Request) XXX_Size() int { return xxx_messageInfo_Request.Size(m) @@ -167,16 +171,17 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_api_17a7876430d97ebd, []int{2} + return fileDescriptor_7b6696ef87ec1943, []int{2} } + func (m *Response) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Response.Unmarshal(m, b) } func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Response.Marshal(b, m, deterministic) } -func (dst *Response) XXX_Merge(src proto.Message) { - xxx_messageInfo_Response.Merge(dst, src) +func (m *Response) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response.Merge(m, src) } func (m *Response) XXX_Size() int { return xxx_messageInfo_Response.Size(m) @@ -230,16 +235,17 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_api_17a7876430d97ebd, []int{3} + return fileDescriptor_7b6696ef87ec1943, []int{3} } + func (m *Event) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Event.Unmarshal(m, b) } func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Event.Marshal(b, m, deterministic) } -func (dst *Event) XXX_Merge(src proto.Message) { - xxx_messageInfo_Event.Merge(dst, src) +func (m *Event) XXX_Merge(src proto.Message) { + xxx_messageInfo_Event.Merge(m, src) } func (m *Event) XXX_Size() int { return xxx_messageInfo_Event.Size(m) @@ -298,35 +304,35 @@ func init() { } func init() { - proto.RegisterFile("github.com/micro/go-micro/api/proto/api.proto", fileDescriptor_api_17a7876430d97ebd) + proto.RegisterFile("github.com/micro/go-micro/api/proto/api.proto", fileDescriptor_7b6696ef87ec1943) } -var fileDescriptor_api_17a7876430d97ebd = []byte{ - // 410 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0xc1, 0x6e, 0xd4, 0x30, - 0x10, 0x55, 0xe2, 0x24, 0x6d, 0x66, 0x11, 0x42, 0x3e, 0x20, 0x53, 0x2a, 0xb4, 0xca, 0x85, 0x15, - 0x52, 0x13, 0x68, 0x39, 0x20, 0xae, 0xb0, 0x2a, 0xc7, 0xca, 0x7f, 0xe0, 0x6d, 0xac, 0xc4, 0x62, - 0x13, 0x9b, 0xd8, 0xa9, 0xb4, 0x1f, 0xc7, 0x81, 0xcf, 0xe0, 0x6f, 0x90, 0x27, 0xde, 0xdd, 0xb2, - 0x5a, 0x2e, 0x74, 0x6f, 0x2f, 0xf6, 0x9b, 0x37, 0x6f, 0xde, 0x38, 0xf0, 0xb6, 0x51, 0xae, 0x1d, - 0x57, 0xe5, 0xbd, 0xee, 0xaa, 0x4e, 0xdd, 0x0f, 0xba, 0x6a, 0xf4, 0x95, 0x30, 0xaa, 0x32, 0x83, - 0x76, 0xba, 0x12, 0x46, 0x95, 0x88, 0x68, 0xd6, 0xe8, 0x52, 0x18, 0x55, 0xbc, 0x87, 0xe4, 0x4e, - 0xa8, 0x81, 0xbe, 0x00, 0xf2, 0x5d, 0x6e, 0x58, 0x34, 0x8f, 0x16, 0x39, 0xf7, 0x90, 0xbe, 0x84, - 0xec, 0x41, 0xac, 0x47, 0x69, 0x59, 0x3c, 0x27, 0x8b, 0x9c, 0x87, 0xaf, 0xe2, 0x17, 0x81, 0x33, - 0x2e, 0x7f, 0x8c, 0xd2, 0x3a, 0xcf, 0xe9, 0xa4, 0x6b, 0x75, 0x1d, 0x0a, 0xc3, 0x17, 0xa5, 0x90, - 0x18, 0xe1, 0x5a, 0x16, 0xe3, 0x29, 0x62, 0x7a, 0x03, 0x59, 0x2b, 0x45, 0x2d, 0x07, 0x46, 0xe6, - 0x64, 0x31, 0xbb, 0x7e, 0x5d, 0x4e, 0x16, 0xca, 0x20, 0x56, 0x7e, 0xc3, 0xdb, 0x65, 0xef, 0x86, - 0x0d, 0x0f, 0x54, 0xfa, 0x0e, 0x48, 0x23, 0x1d, 0x4b, 0xb0, 0x82, 0x1d, 0x56, 0xdc, 0x4a, 0x37, - 0xd1, 0x3d, 0x89, 0x5e, 0x41, 0x62, 0xb4, 0x75, 0x2c, 0x45, 0xf2, 0xab, 0x43, 0xf2, 0x9d, 0xb6, - 0x81, 0x8d, 0x34, 0xef, 0x71, 0xa5, 0xeb, 0x0d, 0xcb, 0x26, 0x8f, 0x1e, 0xfb, 0x14, 0xc6, 0x61, - 0xcd, 0xce, 0xa6, 0x14, 0xc6, 0x61, 0x7d, 0x71, 0x0b, 0xb3, 0x47, 0xbe, 0x8e, 0xc4, 0x54, 0x40, - 0x8a, 0xc1, 0xe0, 0xac, 0xb3, 0xeb, 0x67, 0xdb, 0xb6, 0x3e, 0x55, 0x3e, 0x5d, 0x7d, 0x8e, 0x3f, - 0x45, 0x17, 0x5f, 0xe1, 0x7c, 0x6b, 0xf7, 0x09, 0x2a, 0x4b, 0xc8, 0x77, 0x73, 0xfc, 0xbf, 0x4c, - 0xf1, 0x33, 0x82, 0x73, 0x2e, 0xad, 0xd1, 0xbd, 0x95, 0xf4, 0x0d, 0x80, 0x75, 0xc2, 0x8d, 0xf6, - 0x8b, 0xae, 0x25, 0xaa, 0xa5, 0xfc, 0xd1, 0x09, 0xfd, 0xb8, 0x5b, 0x5c, 0x8c, 0xc9, 0x5e, 0xee, - 0x93, 0x9d, 0x14, 0x8e, 0x6e, 0x6e, 0x1b, 0x2f, 0xd9, 0xc7, 0x7b, 0xb2, 0x30, 0x8b, 0xdf, 0x11, - 0xa4, 0xcb, 0x07, 0xd9, 0xe3, 0x16, 0x7b, 0xd1, 0xc9, 0x20, 0x82, 0x98, 0x3e, 0x87, 0x58, 0xd5, - 0xe1, 0xed, 0xc5, 0xaa, 0xa6, 0x97, 0x90, 0x3b, 0xd5, 0x49, 0xeb, 0x44, 0x67, 0xd0, 0x0f, 0xe1, - 0xfb, 0x03, 0xfa, 0x61, 0x37, 0x5e, 0xf2, 0xf7, 0xc3, 0xc1, 0x06, 0xff, 0x9a, 0xad, 0x16, 0x4e, - 0xb0, 0x74, 0x6a, 0xea, 0xf1, 0xc9, 0x66, 0x5b, 0x65, 0xf8, 0x83, 0xde, 0xfc, 0x09, 0x00, 0x00, - 0xff, 0xff, 0x7a, 0xb4, 0xd4, 0x8f, 0xcb, 0x03, 0x00, 0x00, +var fileDescriptor_7b6696ef87ec1943 = []byte{ + // 408 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x53, 0x4d, 0x8f, 0xd3, 0x30, + 0x10, 0x55, 0xe2, 0x24, 0xbb, 0x99, 0x22, 0x84, 0x7c, 0x40, 0x66, 0x59, 0xa1, 0x2a, 0xa7, 0x0a, + 0xa9, 0x29, 0xec, 0x72, 0x40, 0x5c, 0xa1, 0x5a, 0x8e, 0x2b, 0xff, 0x03, 0x77, 0x63, 0x25, 0x16, + 0x4d, 0x1c, 0x62, 0xa7, 0x52, 0x7f, 0x1c, 0x07, 0x7e, 0x06, 0xff, 0x06, 0x79, 0xec, 0x7e, 0x50, + 0x95, 0x0b, 0xf4, 0xf6, 0x62, 0xbf, 0x79, 0xf3, 0xe6, 0x8d, 0x03, 0xf3, 0x5a, 0xd9, 0x66, 0x5c, + 0x95, 0x4f, 0xba, 0x5d, 0xb4, 0xea, 0x69, 0xd0, 0x8b, 0x5a, 0xcf, 0x3d, 0x10, 0xbd, 0x5a, 0xf4, + 0x83, 0xb6, 0x88, 0x4a, 0x44, 0x34, 0xab, 0x75, 0x29, 0x7a, 0x55, 0xbc, 0x83, 0xe4, 0x51, 0xa8, + 0x81, 0xbe, 0x00, 0xf2, 0x4d, 0x6e, 0x59, 0x34, 0x8d, 0x66, 0x39, 0x77, 0x90, 0xbe, 0x84, 0x6c, + 0x23, 0xd6, 0xa3, 0x34, 0x2c, 0x9e, 0x92, 0x59, 0xce, 0xc3, 0x57, 0xf1, 0x93, 0xc0, 0x15, 0x97, + 0xdf, 0x47, 0x69, 0xac, 0xe3, 0xb4, 0xd2, 0x36, 0xba, 0x0a, 0x85, 0xe1, 0x8b, 0x52, 0x48, 0x7a, + 0x61, 0x1b, 0x16, 0xe3, 0x29, 0x62, 0x7a, 0x0f, 0x59, 0x23, 0x45, 0x25, 0x07, 0x46, 0xa6, 0x64, + 0x36, 0xb9, 0x7b, 0x5d, 0x7a, 0x0b, 0x65, 0x10, 0x2b, 0xbf, 0xe2, 0xed, 0xb2, 0xb3, 0xc3, 0x96, + 0x07, 0x2a, 0x7d, 0x0b, 0xa4, 0x96, 0x96, 0x25, 0x58, 0xc1, 0x4e, 0x2b, 0x1e, 0xa4, 0xf5, 0x74, + 0x47, 0xa2, 0x73, 0x48, 0x7a, 0x6d, 0x2c, 0x4b, 0x91, 0xfc, 0xea, 0x94, 0xfc, 0xa8, 0x4d, 0x60, + 0x23, 0xcd, 0x79, 0x5c, 0xe9, 0x6a, 0xcb, 0x32, 0xef, 0xd1, 0x61, 0x97, 0xc2, 0x38, 0xac, 0xd9, + 0x95, 0x4f, 0x61, 0x1c, 0xd6, 0x37, 0x0f, 0x30, 0x39, 0xf2, 0x75, 0x26, 0xa6, 0x02, 0x52, 0x0c, + 0x06, 0x67, 0x9d, 0xdc, 0x3d, 0xdb, 0xb5, 0x75, 0xa9, 0x72, 0x7f, 0xf5, 0x29, 0xfe, 0x18, 0xdd, + 0x7c, 0x81, 0xeb, 0x9d, 0xdd, 0xff, 0x50, 0x59, 0x42, 0xbe, 0x9f, 0xe3, 0xdf, 0x65, 0x8a, 0x1f, + 0x11, 0x5c, 0x73, 0x69, 0x7a, 0xdd, 0x19, 0x49, 0xdf, 0x00, 0x18, 0x2b, 0xec, 0x68, 0x3e, 0xeb, + 0x4a, 0xa2, 0x5a, 0xca, 0x8f, 0x4e, 0xe8, 0x87, 0xfd, 0xe2, 0x62, 0x4c, 0xf6, 0xf6, 0x90, 0xac, + 0x57, 0x38, 0xbb, 0xb9, 0x5d, 0xbc, 0xe4, 0x10, 0xef, 0xc5, 0xc2, 0x2c, 0x7e, 0x45, 0x90, 0x2e, + 0x37, 0xb2, 0xc3, 0x2d, 0x76, 0xa2, 0x95, 0x41, 0x04, 0x31, 0x7d, 0x0e, 0xb1, 0xaa, 0xc2, 0xdb, + 0x8b, 0x55, 0x45, 0x6f, 0x21, 0xb7, 0xaa, 0x95, 0xc6, 0x8a, 0xb6, 0x47, 0x3f, 0x84, 0x1f, 0x0e, + 0xe8, 0xfb, 0xfd, 0x78, 0xc9, 0x9f, 0x0f, 0x07, 0x1b, 0xfc, 0x6d, 0xb6, 0x4a, 0x58, 0xc1, 0x52, + 0xdf, 0xd4, 0xe1, 0x8b, 0xcd, 0xb6, 0xca, 0xf0, 0x07, 0xbd, 0xff, 0x1d, 0x00, 0x00, 0xff, 0xff, + 0x97, 0xf3, 0x59, 0x6e, 0xd1, 0x03, 0x00, 0x00, } diff --git a/api/proto/api.micro.go b/api/proto/api.pb.micro.go similarity index 59% rename from api/proto/api.micro.go rename to api/proto/api.pb.micro.go index b7b01f8b..a05f98b1 100644 --- a/api/proto/api.micro.go +++ b/api/proto/api.pb.micro.go @@ -1,23 +1,13 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. // source: github.com/micro/go-micro/api/proto/api.proto -/* -Package go_api is a generated protocol buffer package. - -It is generated from these files: - github.com/micro/go-micro/api/proto/api.proto - -It has these top-level messages: - Pair - Request - Response - Event -*/ package go_api -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal @@ -28,4 +18,4 @@ 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.ProtoPackageIsVersion2 // please upgrade the proto package +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package diff --git a/api/server/acme/acme.go b/api/server/acme/acme.go new file mode 100644 index 00000000..f9df8962 --- /dev/null +++ b/api/server/acme/acme.go @@ -0,0 +1,24 @@ +// Package acme abstracts away various ACME libraries +package acme + +import ( + "errors" + "net" +) + +var ( + // ErrProviderNotImplemented can be returned when attempting to + // instantiate an unimplemented provider + ErrProviderNotImplemented = errors.New("Provider not implemented") +) + +// Provider is a ACME provider interface +type Provider interface { + NewListener(...string) (net.Listener, error) +} + +// The Let's Encrypt ACME endpoints +const ( + LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory" + LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory" +) diff --git a/api/server/acme/autocert/autocert.go b/api/server/acme/autocert/autocert.go new file mode 100644 index 00000000..ad5bf0bd --- /dev/null +++ b/api/server/acme/autocert/autocert.go @@ -0,0 +1,23 @@ +// Package autocert is the ACME provider from golang.org/x/crypto/acme/autocert +// This provider does not take any config. +package autocert + +import ( + "net" + + "github.com/micro/go-micro/api/server/acme" + "golang.org/x/crypto/acme/autocert" +) + +// autoCertACME is the ACME provider from golang.org/x/crypto/acme/autocert +type autocertProvider struct{} + +// NewListener implements acme.Provider +func (a *autocertProvider) NewListener(ACMEHosts ...string) (net.Listener, error) { + return autocert.NewListener(ACMEHosts...), nil +} + +// New returns an autocert acme.Provider +func New() acme.Provider { + return &autocertProvider{} +} diff --git a/api/server/acme/autocert/autocert_test.go b/api/server/acme/autocert/autocert_test.go new file mode 100644 index 00000000..570769df --- /dev/null +++ b/api/server/acme/autocert/autocert_test.go @@ -0,0 +1,16 @@ +package autocert + +import ( + "testing" +) + +func TestAutocert(t *testing.T) { + l := New() + if _, ok := l.(*autocertProvider); !ok { + t.Error("New() didn't return an autocertProvider") + } + // TODO: Travis CI doesn't let us bind :443 + // if _, err := l.NewListener(); err != nil { + // t.Error(err.Error()) + // } +} diff --git a/api/server/acme/certmagic/certmagic.go b/api/server/acme/certmagic/certmagic.go new file mode 100644 index 00000000..4a3a464b --- /dev/null +++ b/api/server/acme/certmagic/certmagic.go @@ -0,0 +1,58 @@ +// Package certmagic is the ACME provider from github.com/mholt/certmagic +package certmagic + +import ( + "log" + "math/rand" + "net" + "time" + + "github.com/mholt/certmagic" + + "github.com/micro/go-micro/api/server/acme" +) + +type certmagicProvider struct { + opts acme.Options +} + +func (c *certmagicProvider) NewListener(ACMEHosts ...string) (net.Listener, error) { + certmagic.Default.CA = c.opts.CA + if c.opts.ChallengeProvider != nil { + // Enabling DNS Challenge disables the other challenges + certmagic.Default.DNSProvider = c.opts.ChallengeProvider + } + if c.opts.OnDemand { + certmagic.Default.OnDemand = new(certmagic.OnDemandConfig) + } + if c.opts.Cache != nil { + // already validated by new() + certmagic.Default.Storage = c.opts.Cache.(certmagic.Storage) + } + // If multiple instances of the provider are running, inject some + // randomness so they don't collide + rand.Seed(time.Now().UnixNano()) + randomDuration := (7 * 24 * time.Hour) + (time.Duration(rand.Intn(504)) * time.Hour) + certmagic.Default.RenewDurationBefore = randomDuration + + return certmagic.Listen(ACMEHosts) +} + +// New returns a certmagic provider +func New(options ...acme.Option) acme.Provider { + opts := acme.DefaultOptions() + + for _, o := range options { + o(&opts) + } + + if opts.Cache != nil { + if _, ok := opts.Cache.(certmagic.Storage); !ok { + log.Fatal("ACME: cache provided doesn't implement certmagic's Storage interface") + } + } + + return &certmagicProvider{ + opts: opts, + } +} diff --git a/api/server/acme/certmagic/certmagic_test.go b/api/server/acme/certmagic/certmagic_test.go new file mode 100644 index 00000000..1739f5f2 --- /dev/null +++ b/api/server/acme/certmagic/certmagic_test.go @@ -0,0 +1,224 @@ +package certmagic + +import ( + "net/http" + "os" + "reflect" + "sort" + "testing" + "time" + + "github.com/go-acme/lego/v3/providers/dns/cloudflare" + "github.com/mholt/certmagic" + "github.com/micro/go-micro/api/server/acme" + cfstore "github.com/micro/go-micro/store/cloudflare" + "github.com/micro/go-micro/sync/lock/memory" +) + +func TestCertMagic(t *testing.T) { + if len(os.Getenv("IN_TRAVIS_CI")) != 0 { + t.Skip("Travis doesn't let us bind :443") + } + l, err := New().NewListener() + if err != nil { + t.Fatal(err.Error()) + } + l.Close() + + c := cloudflare.NewDefaultConfig() + c.AuthEmail = "" + c.AuthKey = "" + c.AuthToken = "test" + c.ZoneToken = "test" + + p, err := cloudflare.NewDNSProviderConfig(c) + if err != nil { + t.Fatal(err.Error()) + } + + l, err = New(acme.AcceptToS(true), + acme.CA(acme.LetsEncryptStagingCA), + acme.ChallengeProvider(p), + ).NewListener() + + if err != nil { + t.Fatal(err.Error()) + } + l.Close() +} + +func TestStorageImplementation(t *testing.T) { + apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID") + kvID := os.Getenv("KV_NAMESPACE_ID") + if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 { + t.Skip("No Cloudflare API keys available, skipping test") + } + + var s certmagic.Storage + st := cfstore.NewStore( + cfstore.Token(apiToken), + cfstore.Account(accountID), + cfstore.Namespace(kvID), + ) + s = &storage{ + lock: memory.NewLock(), + store: st, + } + + // Test Lock + if err := s.Lock("test"); err != nil { + t.Fatal(err) + } + + // Test Unlock + if err := s.Unlock("test"); err != nil { + t.Fatal(err) + } + + // Test data + testdata := []struct { + key string + value []byte + }{ + {key: "/foo/a", value: []byte("lorem")}, + {key: "/foo/b", value: []byte("ipsum")}, + {key: "/foo/c", value: []byte("dolor")}, + {key: "/foo/d", value: []byte("sit")}, + {key: "/bar/a", value: []byte("amet")}, + {key: "/bar/b", value: []byte("consectetur")}, + {key: "/bar/c", value: []byte("adipiscing")}, + {key: "/bar/d", value: []byte("elit")}, + {key: "/foo/bar/a", value: []byte("sed")}, + {key: "/foo/bar/b", value: []byte("do")}, + {key: "/foo/bar/c", value: []byte("eiusmod")}, + {key: "/foo/bar/d", value: []byte("tempor")}, + {key: "/foo/bar/baz/a", value: []byte("incididunt")}, + {key: "/foo/bar/baz/b", value: []byte("ut")}, + {key: "/foo/bar/baz/c", value: []byte("labore")}, + {key: "/foo/bar/baz/d", value: []byte("et")}, + // a duplicate just in case there's any edge cases + {key: "/foo/a", value: []byte("lorem")}, + } + + // Test Store + for _, d := range testdata { + if err := s.Store(d.key, d.value); err != nil { + t.Fatal(err.Error()) + } + } + + // Test Load + for _, d := range testdata { + if value, err := s.Load(d.key); err != nil { + t.Fatal(err.Error()) + } else { + if !reflect.DeepEqual(value, d.value) { + t.Fatalf("Load %s: expected %v, got %v", d.key, d.value, value) + } + } + } + + // Test Exists + for _, d := range testdata { + if !s.Exists(d.key) { + t.Fatalf("%s should exist, but doesn't\n", d.key) + } + } + + // Test List + if list, err := s.List("/", true); err != nil { + t.Fatal(err.Error()) + } else { + var expected []string + for i, d := range testdata { + if i != len(testdata)-1 { + // Don't store the intentionally duplicated key + expected = append(expected, d.key) + } + } + sort.Strings(expected) + sort.Strings(list) + if !reflect.DeepEqual(expected, list) { + t.Fatalf("List: Expected %v, got %v\n", expected, list) + } + } + if list, err := s.List("/foo", false); err != nil { + t.Fatal(err.Error()) + } else { + sort.Strings(list) + expected := []string{"/foo/a", "/foo/b", "/foo/bar", "/foo/c", "/foo/d"} + if !reflect.DeepEqual(expected, list) { + t.Fatalf("List: expected %s, got %s\n", expected, list) + } + } + + // Test Stat + for _, d := range testdata { + info, err := s.Stat(d.key) + if err != nil { + t.Fatal(err.Error()) + } else { + if info.Key != d.key { + t.Fatalf("Stat().Key: expected %s, got %s\n", d.key, info.Key) + } + if info.Size != int64(len(d.value)) { + t.Fatalf("Stat().Size: expected %d, got %d\n", len(d.value), info.Size) + } + if time.Since(info.Modified) > time.Minute { + t.Fatalf("Stat().Modified: expected time since last modified to be < 1 minute, got %v\n", time.Since(info.Modified)) + } + } + + } + + // Test Delete + for _, d := range testdata { + if err := s.Delete(d.key); err != nil { + t.Fatal(err.Error()) + } + } + + // New interface doesn't return an error, so call it in case any log.Fatal + // happens + New(acme.Cache(s)) +} + +// Full test with a real zone, with against LE staging +func TestE2e(t *testing.T) { + apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID") + kvID := os.Getenv("KV_NAMESPACE_ID") + if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 { + t.Skip("No Cloudflare API keys available, skipping test") + } + + testLock := memory.NewLock() + testStore := cfstore.NewStore( + cfstore.Token(apiToken), + cfstore.Account(accountID), + cfstore.Namespace(kvID), + ) + testStorage := NewStorage(testLock, testStore) + + conf := cloudflare.NewDefaultConfig() + conf.AuthToken = apiToken + conf.ZoneToken = apiToken + testChallengeProvider, err := cloudflare.NewDNSProviderConfig(conf) + if err != nil { + t.Fatal(err.Error()) + } + + testProvider := New( + acme.AcceptToS(true), + acme.Cache(testStorage), + acme.CA(acme.LetsEncryptStagingCA), + acme.ChallengeProvider(testChallengeProvider), + acme.OnDemand(false), + ) + + listener, err := testProvider.NewListener("*.micro.mu", "micro.mu") + if err != nil { + t.Fatal(err.Error()) + } + go http.Serve(listener, http.NotFoundHandler()) + time.Sleep(10 * time.Minute) +} diff --git a/api/server/acme/certmagic/storage.go b/api/server/acme/certmagic/storage.go new file mode 100644 index 00000000..24d187f5 --- /dev/null +++ b/api/server/acme/certmagic/storage.go @@ -0,0 +1,147 @@ +package certmagic + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "path" + "strings" + "time" + + "github.com/mholt/certmagic" + "github.com/micro/go-micro/store" + "github.com/micro/go-micro/sync/lock" +) + +// File represents a "File" that will be stored in store.Store - the contents and last modified time +type File struct { + // last modified time + LastModified time.Time + // Contents + Contents []byte +} + +// storage is an implementation of certmagic.Storage using micro's sync.Map and store.Store interfaces. +// As certmagic storage expects a filesystem (with stat() abilities) we have to implement +// the bare minimum of metadata. +type storage struct { + lock lock.Lock + store store.Store +} + +func (s *storage) Lock(key string) error { + return s.lock.Acquire(key, lock.TTL(10*time.Minute)) +} + +func (s *storage) Unlock(key string) error { + return s.lock.Release(key) +} + +func (s *storage) Store(key string, value []byte) error { + f := File{ + LastModified: time.Now(), + Contents: value, + } + buf := &bytes.Buffer{} + e := gob.NewEncoder(buf) + if err := e.Encode(f); err != nil { + return err + } + r := &store.Record{ + Key: key, + Value: buf.Bytes(), + } + return s.store.Write(r) +} + +func (s *storage) Load(key string) ([]byte, error) { + if !s.Exists(key) { + return nil, certmagic.ErrNotExist(errors.New(key + " doesn't exist")) + } + records, err := s.store.Read(key) + if err != nil { + return nil, err + } + if len(records) != 1 { + return nil, fmt.Errorf("ACME Storage: multiple records matched key %s", key) + } + b := bytes.NewBuffer(records[0].Value) + d := gob.NewDecoder(b) + var f File + err = d.Decode(&f) + if err != nil { + return nil, err + } + return f.Contents, nil +} + +func (s *storage) Delete(key string) error { + return s.store.Delete(key) +} + +func (s *storage) Exists(key string) bool { + if _, err := s.store.Read(key); err != nil { + return false + } + return true +} + +func (s *storage) List(prefix string, recursive bool) ([]string, error) { + records, err := s.store.List() + if err != nil { + return nil, err + } + + //nolint:prealloc + var results []string + for _, r := range records { + if strings.HasPrefix(r.Key, prefix) { + results = append(results, r.Key) + } + } + if recursive { + return results, nil + } + keysMap := make(map[string]bool) + for _, key := range results { + dir := strings.Split(strings.TrimPrefix(key, prefix+"/"), "/") + keysMap[dir[0]] = true + } + results = make([]string, 0) + for k := range keysMap { + results = append(results, path.Join(prefix, k)) + } + return results, nil +} + +func (s *storage) Stat(key string) (certmagic.KeyInfo, error) { + records, err := s.store.Read(key) + if err != nil { + return certmagic.KeyInfo{}, err + } + if len(records) != 1 { + return certmagic.KeyInfo{}, fmt.Errorf("ACME Storage: multiple records matched key %s", key) + } + b := bytes.NewBuffer(records[0].Value) + d := gob.NewDecoder(b) + var f File + err = d.Decode(&f) + if err != nil { + return certmagic.KeyInfo{}, err + } + return certmagic.KeyInfo{ + Key: key, + Modified: f.LastModified, + Size: int64(len(f.Contents)), + IsTerminal: false, + }, nil +} + +// NewStorage returns a certmagic.Storage backed by a go-micro/lock and go-micro/store +func NewStorage(lock lock.Lock, store store.Store) certmagic.Storage { + return &storage{ + lock: lock, + store: store, + } +} diff --git a/api/server/acme/options.go b/api/server/acme/options.go new file mode 100644 index 00000000..decab3d6 --- /dev/null +++ b/api/server/acme/options.go @@ -0,0 +1,73 @@ +package acme + +import "github.com/go-acme/lego/v3/challenge" + +// Option (or Options) are passed to New() to configure providers +type Option func(o *Options) + +// Options represents various options you can present to ACME providers +type Options struct { + // AcceptTLS must be set to true to indicate that you have read your + // provider's terms of service. + AcceptToS bool + // CA is the CA to use + CA string + // ChallengeProvider is a go-acme/lego challenge provider. Set this if you + // want to use DNS Challenges. Otherwise, tls-alpn-01 will be used + ChallengeProvider challenge.Provider + // Issue certificates for domains on demand. Otherwise, certs will be + // retrieved / issued on start-up. + OnDemand bool + // Cache is a storage interface. Most ACME libraries have an cache, but + // there's no defined interface, so if you consume this option + // sanity check it before using. + Cache interface{} +} + +// AcceptToS indicates whether you accept your CA's terms of service +func AcceptToS(b bool) Option { + return func(o *Options) { + o.AcceptToS = b + } +} + +// CA sets the CA of an acme.Options +func CA(CA string) Option { + return func(o *Options) { + o.CA = CA + } +} + +// ChallengeProvider sets the Challenge provider of an acme.Options +// if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used. +func ChallengeProvider(p challenge.Provider) Option { + return func(o *Options) { + o.ChallengeProvider = p + } +} + +// OnDemand enables on-demand certificate issuance. Not recommended for use +// with the DNS challenge, as the first connection may be very slow. +func OnDemand(b bool) Option { + return func(o *Options) { + o.OnDemand = b + } +} + +// Cache provides a cache / storage interface to the underlying ACME library +// as there is no standard, this needs to be validated by the underlying +// implentation. +func Cache(c interface{}) Option { + return func(o *Options) { + o.Cache = c + } +} + +// DefaultOptions uses the Let's Encrypt Production CA, with DNS Challenge disabled. +func DefaultOptions() Options { + return Options{ + AcceptToS: true, + CA: LetsEncryptProductionCA, + OnDemand: true, + } +} diff --git a/api/server/http/http.go b/api/server/http/http.go index 0990dd8f..3ff93b1a 100644 --- a/api/server/http/http.go +++ b/api/server/http/http.go @@ -11,7 +11,6 @@ import ( "github.com/gorilla/handlers" "github.com/micro/go-micro/api/server" "github.com/micro/go-micro/util/log" - "golang.org/x/crypto/acme/autocert" ) type httpServer struct { @@ -53,9 +52,9 @@ func (s *httpServer) Start() error { var l net.Listener var err error - if s.opts.EnableACME { + if s.opts.EnableACME && s.opts.ACMEProvider != nil { // should we check the address to make sure its using :443? - l = autocert.NewListener(s.opts.ACMEHosts...) + l, err = s.opts.ACMEProvider.NewListener(s.opts.ACMEHosts...) } else if s.opts.EnableTLS && s.opts.TLSConfig != nil { l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig) } else { diff --git a/api/server/options.go b/api/server/options.go index cd47562f..b94c3da8 100644 --- a/api/server/options.go +++ b/api/server/options.go @@ -2,15 +2,24 @@ package server import ( "crypto/tls" + + "github.com/micro/go-micro/api/server/acme" ) type Option func(o *Options) type Options struct { - EnableACME bool - EnableTLS bool - ACMEHosts []string - TLSConfig *tls.Config + EnableACME bool + ACMEProvider acme.Provider + EnableTLS bool + ACMEHosts []string + TLSConfig *tls.Config +} + +func EnableACME(b bool) Option { + return func(o *Options) { + o.EnableACME = b + } } func ACMEHosts(hosts ...string) Option { @@ -19,9 +28,9 @@ func ACMEHosts(hosts ...string) Option { } } -func EnableACME(b bool) Option { +func ACMEProvider(p acme.Provider) Option { return func(o *Options) { - o.EnableACME = b + o.ACMEProvider = p } } diff --git a/broker/common_test.go b/broker/common_test.go index 262a77eb..c01d42c7 100644 --- a/broker/common_test.go +++ b/broker/common_test.go @@ -7,7 +7,7 @@ import ( var ( // mock data testData = map[string][]*registry.Service{ - "foo": []*registry.Service{ + "foo": { { Name: "foo", Version: "1.0.0", diff --git a/broker/http_broker.go b/broker/http_broker.go index 5b362c14..53dd48f1 100644 --- a/broker/http_broker.go +++ b/broker/http_broker.go @@ -64,6 +64,7 @@ type httpEvent struct { var ( DefaultSubPath = "/_sub" + serviceName = "go.micro.http.broker" broadcastVersion = "ff.http.broadcast" registerTTL = time.Minute registerInterval = time.Second * 30 @@ -126,7 +127,7 @@ func newHttpBroker(opts ...Option) Broker { } h := &httpBroker{ - id: "broker-" + uuid.New().String(), + id: uuid.New().String(), address: addr, opts: options, r: reg, @@ -236,12 +237,13 @@ func (h *httpBroker) unsubscribe(s *httpSubscriber) error { h.Lock() defer h.Unlock() + //nolint:prealloc var subscribers []*httpSubscriber // look for subscriber for _, sub := range h.subscribers[s.topic] { // deregister and skip forward - if sub.id == s.id { + if sub == s { _ = h.r.Deregister(sub.svc) continue } @@ -324,15 +326,22 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) { p := &httpEvent{m: m, t: topic} id := req.Form.Get("id") + //nolint:prealloc + var subs []Handler + h.RLock() for _, subscriber := range h.subscribers[topic] { - if id == subscriber.id { - // sub is sync; crufty rate limiting - // so we don't hose the cpu - subscriber.fn(p) + if id != subscriber.id { + continue } + subs = append(subs, subscriber.fn) } h.RUnlock() + + // execute the handler + for _, fn := range subs { + fn(p) + } } func (h *httpBroker) Address() string { @@ -420,7 +429,6 @@ func (h *httpBroker) Connect() error { } func (h *httpBroker) Disconnect() error { - h.RLock() if !h.running { h.RUnlock() @@ -467,7 +475,7 @@ func (h *httpBroker) Init(opts ...Option) error { } if len(h.id) == 0 { - h.id = "broker-" + uuid.New().String() + h.id = "go.micro.http.broker-" + uuid.New().String() } // get registry @@ -522,7 +530,7 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) // now attempt to get the service h.RLock() - s, err := h.r.GetService("topic:" + topic) + s, err := h.r.GetService(serviceName) if err != nil { h.RUnlock() // ignore error @@ -555,8 +563,24 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) srv := func(s []*registry.Service, b []byte) { for _, service := range s { + var nodes []*registry.Node + + for _, node := range service.Nodes { + // only use nodes tagged with broker http + if node.Metadata["broker"] != "http" { + continue + } + + // look for nodes for the topic + if node.Metadata["topic"] != topic { + continue + } + + nodes = append(nodes, node) + } + // only process if we have nodes - if len(service.Nodes) == 0 { + if len(nodes) == 0 { continue } @@ -566,7 +590,7 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) var success bool // publish to all nodes - for _, node := range service.Nodes { + for _, node := range nodes { // publish async if err := pub(node, topic, b); err == nil { success = true @@ -579,7 +603,7 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) } default: // select node to publish to - node := service.Nodes[rand.Int()%len(service.Nodes)] + node := nodes[rand.Int()%len(nodes)] // publish async to one node if err := pub(node, topic, b); err != nil { @@ -627,9 +651,6 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO return nil, err } - // create unique id - id := h.id + "." + uuid.New().String() - var secure bool if h.opts.Secure || h.opts.TLSConfig != nil { @@ -638,10 +659,12 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO // register service node := ®istry.Node{ - Id: id, + Id: topic + "-" + h.id, Address: mnet.HostPort(addr, port), Metadata: map[string]string{ "secure": fmt.Sprintf("%t", secure), + "broker": "http", + "topic": topic, }, } @@ -652,7 +675,7 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO } service := ®istry.Service{ - Name: "topic:" + topic, + Name: serviceName, Version: version, Nodes: []*registry.Node{node}, } @@ -661,7 +684,7 @@ func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeO subscriber := &httpSubscriber{ opts: options, hb: h, - id: id, + id: node.Id, topic: topic, fn: handler, svc: service, diff --git a/broker/http_broker_test.go b/broker/http_broker_test.go index 4695033e..3b9653fa 100644 --- a/broker/http_broker_test.go +++ b/broker/http_broker_test.go @@ -7,15 +7,13 @@ import ( glog "github.com/go-log/log" "github.com/google/uuid" + "github.com/micro/go-micro/registry" "github.com/micro/go-micro/registry/memory" "github.com/micro/go-micro/util/log" ) -func newTestRegistry() *memory.Registry { - r := memory.NewRegistry() - m := r.(*memory.Registry) - m.Services = testData - return m +func newTestRegistry() registry.Registry { + return memory.NewRegistry(memory.Services(testData)) } func sub(be *testing.B, c int) { @@ -125,7 +123,7 @@ func pub(be *testing.B, c int) { for i := 0; i < c; i++ { go func() { - for _ = range ch { + for range ch { if err := b.Publish(topic, msg); err != nil { be.Fatalf("Unexpected publish error: %v", err) } diff --git a/broker/nats/nats.go b/broker/nats/nats.go index eecb16ec..a72f7682 100644 --- a/broker/nats/nats.go +++ b/broker/nats/nats.go @@ -70,6 +70,7 @@ func (n *natsBroker) Address() string { } func setAddrs(addrs []string) []string { + //nolint:prealloc var cAddrs []string for _, addr := range addrs { if len(addr) == 0 { diff --git a/broker/nats/nats_test.go b/broker/nats/nats_test.go index 36625da2..e58362e5 100644 --- a/broker/nats/nats_test.go +++ b/broker/nats/nats_test.go @@ -94,6 +94,5 @@ func TestInitAddrs(t *testing.T) { } } }) - } } diff --git a/broker/service/proto/broker.pb.go b/broker/service/proto/broker.pb.go new file mode 100644 index 00000000..e602e4b1 --- /dev/null +++ b/broker/service/proto/broker.pb.go @@ -0,0 +1,229 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: micro/go-micro/broker/service/proto/broker.proto + +package go_micro_broker + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// 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 + +type Empty struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Empty) Reset() { *m = Empty{} } +func (m *Empty) String() string { return proto.CompactTextString(m) } +func (*Empty) ProtoMessage() {} +func (*Empty) Descriptor() ([]byte, []int) { + return fileDescriptor_178fdc60944ff5e5, []int{0} +} + +func (m *Empty) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Empty.Unmarshal(m, b) +} +func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Empty.Marshal(b, m, deterministic) +} +func (m *Empty) XXX_Merge(src proto.Message) { + xxx_messageInfo_Empty.Merge(m, src) +} +func (m *Empty) XXX_Size() int { + return xxx_messageInfo_Empty.Size(m) +} +func (m *Empty) XXX_DiscardUnknown() { + xxx_messageInfo_Empty.DiscardUnknown(m) +} + +var xxx_messageInfo_Empty proto.InternalMessageInfo + +type PublishRequest struct { + Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"` + Message *Message `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PublishRequest) Reset() { *m = PublishRequest{} } +func (m *PublishRequest) String() string { return proto.CompactTextString(m) } +func (*PublishRequest) ProtoMessage() {} +func (*PublishRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_178fdc60944ff5e5, []int{1} +} + +func (m *PublishRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PublishRequest.Unmarshal(m, b) +} +func (m *PublishRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PublishRequest.Marshal(b, m, deterministic) +} +func (m *PublishRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_PublishRequest.Merge(m, src) +} +func (m *PublishRequest) XXX_Size() int { + return xxx_messageInfo_PublishRequest.Size(m) +} +func (m *PublishRequest) XXX_DiscardUnknown() { + xxx_messageInfo_PublishRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_PublishRequest proto.InternalMessageInfo + +func (m *PublishRequest) GetTopic() string { + if m != nil { + return m.Topic + } + return "" +} + +func (m *PublishRequest) GetMessage() *Message { + if m != nil { + return m.Message + } + return nil +} + +type SubscribeRequest struct { + Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"` + Queue string `protobuf:"bytes,2,opt,name=queue,proto3" json:"queue,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SubscribeRequest) Reset() { *m = SubscribeRequest{} } +func (m *SubscribeRequest) String() string { return proto.CompactTextString(m) } +func (*SubscribeRequest) ProtoMessage() {} +func (*SubscribeRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_178fdc60944ff5e5, []int{2} +} + +func (m *SubscribeRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SubscribeRequest.Unmarshal(m, b) +} +func (m *SubscribeRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SubscribeRequest.Marshal(b, m, deterministic) +} +func (m *SubscribeRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_SubscribeRequest.Merge(m, src) +} +func (m *SubscribeRequest) XXX_Size() int { + return xxx_messageInfo_SubscribeRequest.Size(m) +} +func (m *SubscribeRequest) XXX_DiscardUnknown() { + xxx_messageInfo_SubscribeRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_SubscribeRequest proto.InternalMessageInfo + +func (m *SubscribeRequest) GetTopic() string { + if m != nil { + return m.Topic + } + return "" +} + +func (m *SubscribeRequest) GetQueue() string { + if m != nil { + return m.Queue + } + return "" +} + +type Message struct { + Header map[string]string `protobuf:"bytes,1,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Message) Reset() { *m = Message{} } +func (m *Message) String() string { return proto.CompactTextString(m) } +func (*Message) ProtoMessage() {} +func (*Message) Descriptor() ([]byte, []int) { + return fileDescriptor_178fdc60944ff5e5, []int{3} +} + +func (m *Message) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Message.Unmarshal(m, b) +} +func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Message.Marshal(b, m, deterministic) +} +func (m *Message) XXX_Merge(src proto.Message) { + xxx_messageInfo_Message.Merge(m, src) +} +func (m *Message) XXX_Size() int { + return xxx_messageInfo_Message.Size(m) +} +func (m *Message) XXX_DiscardUnknown() { + xxx_messageInfo_Message.DiscardUnknown(m) +} + +var xxx_messageInfo_Message proto.InternalMessageInfo + +func (m *Message) GetHeader() map[string]string { + if m != nil { + return m.Header + } + return nil +} + +func (m *Message) GetBody() []byte { + if m != nil { + return m.Body + } + return nil +} + +func init() { + proto.RegisterType((*Empty)(nil), "go.micro.broker.Empty") + proto.RegisterType((*PublishRequest)(nil), "go.micro.broker.PublishRequest") + proto.RegisterType((*SubscribeRequest)(nil), "go.micro.broker.SubscribeRequest") + proto.RegisterType((*Message)(nil), "go.micro.broker.Message") + proto.RegisterMapType((map[string]string)(nil), "go.micro.broker.Message.HeaderEntry") +} + +func init() { + proto.RegisterFile("micro/go-micro/broker/service/proto/broker.proto", fileDescriptor_178fdc60944ff5e5) +} + +var fileDescriptor_178fdc60944ff5e5 = []byte{ + // 305 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x51, 0x4d, 0x4f, 0xc2, 0x40, + 0x14, 0x64, 0x41, 0x68, 0x78, 0x18, 0x25, 0x1b, 0x62, 0x1a, 0x2e, 0x62, 0xe3, 0x81, 0x8b, 0x5b, + 0x52, 0x2f, 0x6a, 0x8c, 0x07, 0x23, 0x89, 0x07, 0x4d, 0xcc, 0x7a, 0xf3, 0xd6, 0x2d, 0x2f, 0xa5, + 0x81, 0xba, 0x65, 0xb7, 0x25, 0xe9, 0x1f, 0xf1, 0xe4, 0x8f, 0x35, 0xec, 0x16, 0x3f, 0x68, 0xf0, + 0x36, 0xf3, 0x76, 0x76, 0xde, 0x64, 0x1e, 0x4c, 0xd2, 0x24, 0x52, 0xd2, 0x8f, 0xe5, 0x85, 0x05, + 0x42, 0xc9, 0x05, 0x2a, 0x5f, 0xa3, 0x5a, 0x27, 0x11, 0xfa, 0x99, 0x92, 0xf9, 0x76, 0xc8, 0x0c, + 0xa1, 0xc7, 0xb1, 0x64, 0x46, 0xcb, 0xec, 0xd8, 0x73, 0xa0, 0x3d, 0x4d, 0xb3, 0xbc, 0xf4, 0xde, + 0xe0, 0xe8, 0xa5, 0x10, 0xcb, 0x44, 0xcf, 0x39, 0xae, 0x0a, 0xd4, 0x39, 0x1d, 0x40, 0x3b, 0x97, + 0x59, 0x12, 0xb9, 0x64, 0x44, 0xc6, 0x5d, 0x6e, 0x09, 0x0d, 0xc0, 0x49, 0x51, 0xeb, 0x30, 0x46, + 0xb7, 0x39, 0x22, 0xe3, 0x5e, 0xe0, 0xb2, 0x1d, 0x4f, 0xf6, 0x6c, 0xdf, 0xf9, 0x56, 0xe8, 0xdd, + 0x41, 0xff, 0xb5, 0x10, 0x3a, 0x52, 0x89, 0xc0, 0xff, 0xdd, 0x07, 0xd0, 0x5e, 0x15, 0x58, 0x58, + 0xef, 0x2e, 0xb7, 0xc4, 0xfb, 0x20, 0xe0, 0x54, 0xa6, 0xf4, 0x16, 0x3a, 0x73, 0x0c, 0x67, 0xa8, + 0x5c, 0x32, 0x6a, 0x8d, 0x7b, 0xc1, 0xf9, 0xbe, 0xf5, 0xec, 0xd1, 0xc8, 0xa6, 0xef, 0xb9, 0x2a, + 0x79, 0xf5, 0x87, 0x52, 0x38, 0x10, 0x72, 0x56, 0x1a, 0xfb, 0x43, 0x6e, 0xf0, 0xf0, 0x1a, 0x7a, + 0xbf, 0xa4, 0xb4, 0x0f, 0xad, 0x05, 0x96, 0x55, 0xac, 0x0d, 0xdc, 0x84, 0x5a, 0x87, 0xcb, 0x9f, + 0x50, 0x86, 0xdc, 0x34, 0xaf, 0x48, 0xf0, 0x49, 0xa0, 0x73, 0x6f, 0xb6, 0xd2, 0x07, 0x70, 0xaa, + 0xfe, 0xe8, 0x69, 0x2d, 0xd2, 0xdf, 0x66, 0x87, 0x27, 0x35, 0x81, 0xbd, 0x41, 0x83, 0x3e, 0x41, + 0xf7, 0xbb, 0x29, 0x7a, 0x56, 0x93, 0xed, 0xb6, 0x38, 0xdc, 0x5b, 0xbe, 0xd7, 0x98, 0x10, 0xd1, + 0x31, 0x47, 0xbf, 0xfc, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x60, 0x8c, 0x40, 0xd5, 0x28, 0x02, 0x00, + 0x00, +} diff --git a/broker/service/proto/broker.pb.micro.go b/broker/service/proto/broker.pb.micro.go new file mode 100644 index 00000000..c0229fb6 --- /dev/null +++ b/broker/service/proto/broker.pb.micro.go @@ -0,0 +1,173 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: micro/go-micro/broker/service/proto/broker.proto + +package go_micro_broker + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +import ( + context "context" + client "github.com/micro/go-micro/client" + server "github.com/micro/go-micro/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// 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 + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ client.Option +var _ server.Option + +// Client API for Broker service + +type BrokerService interface { + Publish(ctx context.Context, in *PublishRequest, opts ...client.CallOption) (*Empty, error) + Subscribe(ctx context.Context, in *SubscribeRequest, opts ...client.CallOption) (Broker_SubscribeService, error) +} + +type brokerService struct { + c client.Client + name string +} + +func NewBrokerService(name string, c client.Client) BrokerService { + if c == nil { + c = client.NewClient() + } + if len(name) == 0 { + name = "go.micro.broker" + } + return &brokerService{ + c: c, + name: name, + } +} + +func (c *brokerService) Publish(ctx context.Context, in *PublishRequest, opts ...client.CallOption) (*Empty, error) { + req := c.c.NewRequest(c.name, "Broker.Publish", in) + out := new(Empty) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *brokerService) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...client.CallOption) (Broker_SubscribeService, error) { + req := c.c.NewRequest(c.name, "Broker.Subscribe", &SubscribeRequest{}) + stream, err := c.c.Stream(ctx, req, opts...) + if err != nil { + return nil, err + } + if err := stream.Send(in); err != nil { + return nil, err + } + return &brokerServiceSubscribe{stream}, nil +} + +type Broker_SubscribeService interface { + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Recv() (*Message, error) +} + +type brokerServiceSubscribe struct { + stream client.Stream +} + +func (x *brokerServiceSubscribe) Close() error { + return x.stream.Close() +} + +func (x *brokerServiceSubscribe) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *brokerServiceSubscribe) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *brokerServiceSubscribe) Recv() (*Message, error) { + m := new(Message) + err := x.stream.Recv(m) + if err != nil { + return nil, err + } + return m, nil +} + +// Server API for Broker service + +type BrokerHandler interface { + Publish(context.Context, *PublishRequest, *Empty) error + Subscribe(context.Context, *SubscribeRequest, Broker_SubscribeStream) error +} + +func RegisterBrokerHandler(s server.Server, hdlr BrokerHandler, opts ...server.HandlerOption) error { + type broker interface { + Publish(ctx context.Context, in *PublishRequest, out *Empty) error + Subscribe(ctx context.Context, stream server.Stream) error + } + type Broker struct { + broker + } + h := &brokerHandler{hdlr} + return s.Handle(s.NewHandler(&Broker{h}, opts...)) +} + +type brokerHandler struct { + BrokerHandler +} + +func (h *brokerHandler) Publish(ctx context.Context, in *PublishRequest, out *Empty) error { + return h.BrokerHandler.Publish(ctx, in, out) +} + +func (h *brokerHandler) Subscribe(ctx context.Context, stream server.Stream) error { + m := new(SubscribeRequest) + if err := stream.Recv(m); err != nil { + return err + } + return h.BrokerHandler.Subscribe(ctx, m, &brokerSubscribeStream{stream}) +} + +type Broker_SubscribeStream interface { + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Send(*Message) error +} + +type brokerSubscribeStream struct { + stream server.Stream +} + +func (x *brokerSubscribeStream) Close() error { + return x.stream.Close() +} + +func (x *brokerSubscribeStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *brokerSubscribeStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *brokerSubscribeStream) Send(m *Message) error { + return x.stream.Send(m) +} diff --git a/broker/service/proto/broker.proto b/broker/service/proto/broker.proto new file mode 100644 index 00000000..3fc877e6 --- /dev/null +++ b/broker/service/proto/broker.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package go.micro.broker; + +service Broker { + rpc Publish(PublishRequest) returns (Empty) {}; + rpc Subscribe(SubscribeRequest) returns (stream Message) {}; +} + +message Empty {} + +message PublishRequest { + string topic = 1; + Message message = 2; +} + +message SubscribeRequest { + string topic = 1; + string queue = 2; +} + +message Message { + map header = 1; + bytes body = 2; +} diff --git a/broker/service/service.go b/broker/service/service.go new file mode 100644 index 00000000..fff207fd --- /dev/null +++ b/broker/service/service.go @@ -0,0 +1,132 @@ +// Package service provides the broker service client +package service + +import ( + "context" + "time" + + "github.com/micro/go-micro/broker" + pb "github.com/micro/go-micro/broker/service/proto" + "github.com/micro/go-micro/client" + "github.com/micro/go-micro/util/log" +) + +type serviceBroker struct { + Addrs []string + Client pb.BrokerService + options broker.Options +} + +var ( + DefaultName = "go.micro.broker" +) + +func (b *serviceBroker) Address() string { + return b.Addrs[0] +} + +func (b *serviceBroker) Connect() error { + return nil +} + +func (b *serviceBroker) Disconnect() error { + return nil +} + +func (b *serviceBroker) Init(opts ...broker.Option) error { + for _, o := range opts { + o(&b.options) + } + return nil +} + +func (b *serviceBroker) Options() broker.Options { + return b.options +} + +func (b *serviceBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { + log.Debugf("Publishing to topic %s broker %v", topic, b.Addrs) + _, err := b.Client.Publish(context.TODO(), &pb.PublishRequest{ + Topic: topic, + Message: &pb.Message{ + Header: msg.Header, + Body: msg.Body, + }, + }, client.WithAddress(b.Addrs...)) + return err +} + +func (b *serviceBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { + var options broker.SubscribeOptions + for _, o := range opts { + o(&options) + } + log.Debugf("Subscribing to topic %s queue %s broker %v", topic, options.Queue, b.Addrs) + stream, err := b.Client.Subscribe(context.TODO(), &pb.SubscribeRequest{ + Topic: topic, + Queue: options.Queue, + }, client.WithAddress(b.Addrs...), client.WithRequestTimeout(time.Hour)) + if err != nil { + return nil, err + } + + sub := &serviceSub{ + topic: topic, + queue: options.Queue, + handler: handler, + stream: stream, + closed: make(chan bool), + options: options, + } + + go func() { + for { + select { + case <-sub.closed: + log.Debugf("Unsubscribed from topic %s", topic) + return + default: + // run the subscriber + log.Debugf("Streaming from broker %v to topic [%s] queue [%s]", b.Addrs, topic, options.Queue) + if err := sub.run(); err != nil { + log.Debugf("Resubscribing to topic %s broker %v", topic, b.Addrs) + stream, err := b.Client.Subscribe(context.TODO(), &pb.SubscribeRequest{ + Topic: topic, + Queue: options.Queue, + }, client.WithAddress(b.Addrs...), client.WithRequestTimeout(time.Hour)) + if err != nil { + log.Debugf("Failed to resubscribe to topic %s: %v", topic, err) + time.Sleep(time.Second) + continue + } + // new stream + sub.stream = stream + } + } + } + }() + + return sub, nil +} + +func (b *serviceBroker) String() string { + return "service" +} + +func NewBroker(opts ...broker.Option) broker.Broker { + var options broker.Options + for _, o := range opts { + o(&options) + } + + addrs := options.Addrs + if len(addrs) == 0 { + addrs = []string{"127.0.0.1:8001"} + } + + return &serviceBroker{ + Addrs: addrs, + Client: pb.NewBrokerService(DefaultName, client.DefaultClient), + options: options, + } +} diff --git a/broker/service/subscriber.go b/broker/service/subscriber.go new file mode 100644 index 00000000..085b578a --- /dev/null +++ b/broker/service/subscriber.go @@ -0,0 +1,101 @@ +package service + +import ( + "github.com/micro/go-micro/broker" + pb "github.com/micro/go-micro/broker/service/proto" + "github.com/micro/go-micro/util/log" +) + +type serviceSub struct { + topic string + queue string + handler broker.Handler + stream pb.Broker_SubscribeService + closed chan bool + options broker.SubscribeOptions +} + +type serviceEvent struct { + topic string + message *broker.Message +} + +func (s *serviceEvent) Topic() string { + return s.topic +} + +func (s *serviceEvent) Message() *broker.Message { + return s.message +} + +func (s *serviceEvent) Ack() error { + return nil +} + +func (s *serviceSub) isClosed() bool { + select { + case <-s.closed: + return true + default: + return false + } +} + +func (s *serviceSub) run() error { + exit := make(chan bool) + go func() { + select { + case <-exit: + case <-s.closed: + } + + // close the stream + s.stream.Close() + }() + + for { + // TODO: do not fail silently + msg, err := s.stream.Recv() + if err != nil { + log.Debugf("Streaming error for subcription to topic %s: %v", s.Topic(), err) + + // close the exit channel + close(exit) + + // don't return an error if we unsubscribed + if s.isClosed() { + return nil + } + + // return stream error + return err + } + + // TODO: handle error + s.handler(&serviceEvent{ + topic: s.topic, + message: &broker.Message{ + Header: msg.Header, + Body: msg.Body, + }, + }) + } +} + +func (s *serviceSub) Options() broker.SubscribeOptions { + return s.options +} + +func (s *serviceSub) Topic() string { + return s.topic +} + +func (s *serviceSub) Unsubscribe() error { + select { + case <-s.closed: + return nil + default: + close(s.closed) + } + return nil +} diff --git a/client/common_test.go b/client/common_test.go index 15ddc158..25b2b09c 100644 --- a/client/common_test.go +++ b/client/common_test.go @@ -7,7 +7,7 @@ import ( var ( // mock data testData = map[string][]*registry.Service{ - "foo": []*registry.Service{ + "foo": { { Name: "foo", Version: "1.0.0", @@ -15,10 +15,16 @@ var ( { Id: "foo-1.0.0-123", Address: "localhost:9999", + Metadata: map[string]string{ + "protocol": "mucp", + }, }, { Id: "foo-1.0.0-321", Address: "localhost:9999", + Metadata: map[string]string{ + "protocol": "mucp", + }, }, }, }, @@ -29,6 +35,9 @@ var ( { Id: "foo-1.0.1-321", Address: "localhost:6666", + Metadata: map[string]string{ + "protocol": "mucp", + }, }, }, }, @@ -39,6 +48,9 @@ var ( { Id: "foo-1.0.3-345", Address: "localhost:8888", + Metadata: map[string]string{ + "protocol": "mucp", + }, }, }, }, diff --git a/client/grpc/codec.go b/client/grpc/codec.go index c792abbd..44f16d54 100644 --- a/client/grpc/codec.go +++ b/client/grpc/codec.go @@ -11,8 +11,6 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/micro/go-micro/codec" "github.com/micro/go-micro/codec/bytes" - "github.com/micro/go-micro/codec/jsonrpc" - "github.com/micro/go-micro/codec/protorpc" "google.golang.org/grpc" "google.golang.org/grpc/encoding" ) @@ -36,14 +34,6 @@ var ( "application/grpc+bytes": bytesCodec{}, } - defaultRPCCodecs = map[string]codec.NewCodec{ - "application/json": jsonrpc.NewCodec, - "application/json-rpc": jsonrpc.NewCodec, - "application/protobuf": protorpc.NewCodec, - "application/proto-rpc": protorpc.NewCodec, - "application/octet-stream": protorpc.NewCodec, - } - json = jsoniter.ConfigCompatibleWithStandardLibrary ) @@ -182,7 +172,7 @@ func (g *grpcCodec) Write(m *codec.Message, v interface{}) error { return g.s.SendMsg(v) } // write the body using the framing codec - return g.s.SendMsg(&bytes.Frame{m.Body}) + return g.s.SendMsg(&bytes.Frame{Data: m.Body}) } func (g *grpcCodec) Close() error { diff --git a/client/grpc/grpc.go b/client/grpc/grpc.go index 439be459..a593b690 100644 --- a/client/grpc/grpc.go +++ b/client/grpc/grpc.go @@ -13,6 +13,7 @@ import ( "github.com/micro/go-micro/client" "github.com/micro/go-micro/client/selector" "github.com/micro/go-micro/codec" + raw "github.com/micro/go-micro/codec/bytes" "github.com/micro/go-micro/errors" "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/registry" @@ -110,12 +111,21 @@ func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.R var grr error - cc, err := g.pool.getConn(address, grpc.WithDefaultCallOptions(grpc.ForceCodec(cf)), - grpc.WithTimeout(opts.DialTimeout), g.secure(), + grpcDialOptions := []grpc.DialOption{ + grpc.WithDefaultCallOptions(grpc.ForceCodec(cf)), + grpc.WithTimeout(opts.DialTimeout), + g.secure(), grpc.WithDefaultCallOptions( grpc.MaxCallRecvMsgSize(maxRecvMsgSize), grpc.MaxCallSendMsgSize(maxSendMsgSize), - )) + ), + } + + if opts := g.getGrpcDialOptions(); opts != nil { + grpcDialOptions = append(grpcDialOptions, opts...) + } + + cc, err := g.pool.getConn(address, grpcDialOptions...) if err != nil { return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err)) } @@ -127,7 +137,11 @@ func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.R ch := make(chan error, 1) go func() { - err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpc.CallContentSubtype(cf.Name())) + grpcCallOptions := []grpc.CallOption{grpc.CallContentSubtype(cf.Name())} + if opts := g.getGrpcCallOptions(); opts != nil { + grpcCallOptions = append(grpcCallOptions, opts...) + } + err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...) ch <- microError(err) }() @@ -175,7 +189,16 @@ func (g *grpcClient) stream(ctx context.Context, node *registry.Node, req client wc := wrapCodec{cf} - cc, err := grpc.DialContext(dialCtx, address, grpc.WithDefaultCallOptions(grpc.ForceCodec(wc)), g.secure()) + grpcDialOptions := []grpc.DialOption{ + grpc.WithDefaultCallOptions(grpc.ForceCodec(wc)), + g.secure(), + } + + if opts := g.getGrpcDialOptions(); opts != nil { + grpcDialOptions = append(grpcDialOptions, opts...) + } + + cc, err := grpc.DialContext(dialCtx, address, grpcDialOptions...) if err != nil { return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err)) } @@ -186,7 +209,11 @@ func (g *grpcClient) stream(ctx context.Context, node *registry.Node, req client ServerStreams: true, } - st, err := cc.NewStream(ctx, desc, methodToGRPC(req.Service(), req.Endpoint())) + grpcCallOptions := []grpc.CallOption{} + if opts := g.getGrpcCallOptions(); opts != nil { + grpcCallOptions = append(grpcCallOptions, opts...) + } + st, err := cc.NewStream(ctx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...) if err != nil { return nil, errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err)) } @@ -255,16 +282,6 @@ func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) { return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) } -func (g *grpcClient) newCodec(contentType string) (codec.NewCodec, error) { - if c, ok := g.opts.Codecs[contentType]; ok { - return c, nil - } - if cf, ok := defaultRPCCodecs[contentType]; ok { - return cf, nil - } - return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) -} - func (g *grpcClient) Init(opts ...client.Option) error { size := g.opts.PoolSize ttl := g.opts.PoolTTL @@ -312,7 +329,9 @@ func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface d, ok := ctx.Deadline() if !ok { // no deadline so we create a new one - ctx, _ = context.WithTimeout(ctx, callOpts.RequestTimeout) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout) + defer cancel() } else { // got a deadline so no need to setup context // but we need to set the timeout we pass along @@ -484,29 +503,56 @@ func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...cli } func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error { + var options client.PublishOptions + for _, o := range opts { + o(&options) + } + md, ok := metadata.FromContext(ctx) if !ok { md = make(map[string]string) } md["Content-Type"] = p.ContentType() + md["Micro-Topic"] = p.Topic() cf, err := g.newGRPCCodec(p.ContentType()) if err != nil { return errors.InternalServerError("go.micro.client", err.Error()) } - b, err := cf.Marshal(p.Payload()) - if err != nil { - return errors.InternalServerError("go.micro.client", err.Error()) + var body []byte + + // passed in raw data + if d, ok := p.Payload().(*raw.Frame); ok { + body = d.Data + } else { + // set the body + b, err := cf.Marshal(p.Payload()) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + body = b } g.once.Do(func() { g.opts.Broker.Connect() }) - return g.opts.Broker.Publish(p.Topic(), &broker.Message{ + topic := p.Topic() + + // get proxy topic + if prx := os.Getenv("MICRO_PROXY"); len(prx) > 0 { + options.Exchange = prx + } + + // get the exchange + if len(options.Exchange) > 0 { + topic = options.Exchange + } + + return g.opts.Broker.Publish(topic, &broker.Message{ Header: md, - Body: b, + Body: body, }) } @@ -514,6 +560,46 @@ func (g *grpcClient) String() string { return "grpc" } +func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption { + if g.opts.CallOptions.Context == nil { + return nil + } + + v := g.opts.CallOptions.Context.Value(grpcDialOptions{}) + + if v == nil { + return nil + } + + opts, ok := v.([]grpc.DialOption) + + if !ok { + return nil + } + + return opts +} + +func (g *grpcClient) getGrpcCallOptions() []grpc.CallOption { + if g.opts.CallOptions.Context == nil { + return nil + } + + v := g.opts.CallOptions.Context.Value(grpcCallOptions{}) + + if v == nil { + return nil + } + + opts, ok := v.([]grpc.CallOption) + + if !ok { + return nil + } + + return opts +} + func newClient(opts ...client.Option) client.Client { options := client.Options{ Codecs: make(map[string]codec.NewCodec), diff --git a/client/grpc/grpc_pool_test.go b/client/grpc/grpc_pool_test.go index 955e18c5..d5c1fdb0 100644 --- a/client/grpc/grpc_pool_test.go +++ b/client/grpc/grpc_pool_test.go @@ -1,11 +1,11 @@ package grpc import ( + "context" "net" "testing" "time" - "context" "google.golang.org/grpc" pgrpc "google.golang.org/grpc" pb "google.golang.org/grpc/examples/helloworld/helloworld" diff --git a/client/grpc/grpc_test.go b/client/grpc/grpc_test.go index 5696a267..6d975ca3 100644 --- a/client/grpc/grpc_test.go +++ b/client/grpc/grpc_test.go @@ -42,9 +42,12 @@ func TestGRPCClient(t *testing.T) { Name: "helloworld", Version: "test", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test-1", Address: l.Addr().String(), + Metadata: map[string]string{ + "protocol": "grpc", + }, }, }, }) diff --git a/client/grpc/options.go b/client/grpc/options.go index c702ade3..e7f2fceb 100644 --- a/client/grpc/options.go +++ b/client/grpc/options.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "github.com/micro/go-micro/client" + "google.golang.org/grpc" "google.golang.org/grpc/encoding" ) @@ -23,6 +24,8 @@ type codecsKey struct{} type tlsAuth struct{} type maxRecvMsgSizeKey struct{} type maxSendMsgSizeKey struct{} +type grpcDialOptions struct{} +type grpcCallOptions struct{} // gRPC Codec to be used to encode/decode requests for a given content type func Codec(contentType string, c encoding.Codec) client.Option { @@ -72,3 +75,27 @@ func MaxSendMsgSize(s int) client.Option { o.Context = context.WithValue(o.Context, maxSendMsgSizeKey{}, s) } } + +// +// DialOptions to be used to configure gRPC dial options +// +func DialOptions(opts ...grpc.DialOption) client.CallOption { + return func(o *client.CallOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, grpcDialOptions{}, opts) + } +} + +// +// CallOptions to be used to configure gRPC call options +// +func CallOptions(opts ...grpc.CallOption) client.CallOption { + return func(o *client.CallOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, grpcCallOptions{}, opts) + } +} diff --git a/client/grpc/stream.go b/client/grpc/stream.go index af919e46..c6b38c15 100644 --- a/client/grpc/stream.go +++ b/client/grpc/stream.go @@ -43,14 +43,12 @@ func (g *grpcStream) Send(msg interface{}) error { func (g *grpcStream) Recv(msg interface{}) (err error) { defer g.setError(err) if err = g.stream.RecvMsg(msg); err != nil { - if err == io.EOF { - // #202 - inconsistent gRPC stream behavior - // the only way to tell if the stream is done is when we get a EOF on the Recv - // here we should close the underlying gRPC ClientConn - closeErr := g.conn.Close() - if closeErr != nil { - err = closeErr - } + // #202 - inconsistent gRPC stream behavior + // the only way to tell if the stream is done is when we get a EOF on the Recv + // here we should close the underlying gRPC ClientConn + closeErr := g.conn.Close() + if err == io.EOF && closeErr != nil { + err = closeErr } } return diff --git a/client/options.go b/client/options.go index 9f363d74..5a7654a4 100644 --- a/client/options.go +++ b/client/options.go @@ -125,6 +125,10 @@ func newOptions(options ...Option) Options { opts.Transport = transport.DefaultTransport } + if opts.Context == nil { + opts.Context = context.Background() + } + return opts } diff --git a/client/proto/client.pb.go b/client/proto/client.pb.go index c923e337..bd5b3702 100644 --- a/client/proto/client.pb.go +++ b/client/proto/client.pb.go @@ -1,13 +1,11 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: micro/go-micro/client/proto/client.proto +// source: github.com/micro/go-micro/client/proto/client.proto package go_micro_client import ( - context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" math "math" ) @@ -36,7 +34,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_7d733ae29171347b, []int{0} + return fileDescriptor_d418333f021a3308, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { @@ -96,7 +94,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_7d733ae29171347b, []int{1} + return fileDescriptor_d418333f021a3308, []int{1} } func (m *Response) XXX_Unmarshal(b []byte) error { @@ -137,7 +135,7 @@ func (m *Message) Reset() { *m = Message{} } func (m *Message) String() string { return proto.CompactTextString(m) } func (*Message) ProtoMessage() {} func (*Message) Descriptor() ([]byte, []int) { - return fileDescriptor_7d733ae29171347b, []int{2} + return fileDescriptor_d418333f021a3308, []int{2} } func (m *Message) XXX_Unmarshal(b []byte) error { @@ -186,203 +184,27 @@ func init() { } func init() { - proto.RegisterFile("micro/go-micro/client/proto/client.proto", fileDescriptor_7d733ae29171347b) + proto.RegisterFile("github.com/micro/go-micro/client/proto/client.proto", fileDescriptor_d418333f021a3308) } -var fileDescriptor_7d733ae29171347b = []byte{ - // 270 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x91, 0x41, 0x4b, 0xc3, 0x40, - 0x10, 0x85, 0xbb, 0x6d, 0x4c, 0xea, 0x58, 0x10, 0x06, 0x0f, 0x6b, 0x0e, 0x52, 0x73, 0xca, 0xc5, - 0x54, 0xf4, 0x2c, 0x1e, 0x72, 0x16, 0x24, 0x8a, 0x57, 0x49, 0xb6, 0x43, 0x5c, 0x48, 0x77, 0xd7, - 0xec, 0xb6, 0x90, 0x1f, 0xe9, 0x7f, 0x12, 0x36, 0xa9, 0x15, 0x6d, 0x2f, 0xbd, 0xcd, 0x9b, 0x6f, - 0x79, 0x33, 0xfb, 0x06, 0xd2, 0x95, 0x14, 0xad, 0x5e, 0xd4, 0xfa, 0xa6, 0x2f, 0x44, 0x23, 0x49, - 0xb9, 0x85, 0x69, 0xb5, 0xdb, 0x8a, 0xcc, 0x0b, 0x3c, 0xaf, 0x75, 0xe6, 0xdf, 0x64, 0x7d, 0x3b, - 0xd9, 0x40, 0x54, 0xd0, 0xe7, 0x9a, 0xac, 0x43, 0x0e, 0x91, 0xa5, 0x76, 0x23, 0x05, 0x71, 0x36, - 0x67, 0xe9, 0x69, 0xb1, 0x95, 0x18, 0xc3, 0x94, 0xd4, 0xd2, 0x68, 0xa9, 0x1c, 0x1f, 0x7b, 0xf4, - 0xa3, 0xf1, 0x1a, 0x66, 0x42, 0x2b, 0x47, 0xca, 0xbd, 0xbb, 0xce, 0x10, 0x9f, 0x78, 0x7e, 0x36, - 0xf4, 0x5e, 0x3b, 0x43, 0x88, 0x10, 0x54, 0x7a, 0xd9, 0xf1, 0x60, 0xce, 0xd2, 0x59, 0xe1, 0xeb, - 0xe4, 0x0a, 0xa6, 0x05, 0x59, 0xa3, 0x95, 0xdd, 0x71, 0xf6, 0x8b, 0xbf, 0x41, 0xf4, 0x44, 0xd6, - 0x96, 0x35, 0xe1, 0x05, 0x9c, 0x38, 0x6d, 0xa4, 0x18, 0xb6, 0xea, 0xc5, 0xbf, 0xb9, 0xe3, 0xc3, - 0x73, 0x27, 0x3b, 0xdf, 0xbb, 0x2f, 0x06, 0x61, 0xee, 0xbf, 0x8e, 0x0f, 0x10, 0xe4, 0x65, 0xd3, - 0x20, 0xcf, 0xfe, 0x84, 0x92, 0x0d, 0x89, 0xc4, 0x97, 0x7b, 0x48, 0xbf, 0x73, 0x32, 0xc2, 0x1c, - 0xc2, 0x17, 0xd7, 0x52, 0xb9, 0x3a, 0xd2, 0x20, 0x65, 0xb7, 0x0c, 0x1f, 0x21, 0x7a, 0x5e, 0x57, - 0x8d, 0xb4, 0x1f, 0x7b, 0x5c, 0x86, 0x00, 0xe2, 0x83, 0x24, 0x19, 0x55, 0xa1, 0xbf, 0xeb, 0xfd, - 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0a, 0x76, 0x1f, 0x51, 0x03, 0x02, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// ClientClient is the client API for Client service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type ClientClient interface { - // Call allows a single request to be made - Call(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) - // Stream is a bidirectional stream - Stream(ctx context.Context, opts ...grpc.CallOption) (Client_StreamClient, error) - // Publish publishes a message and returns an empty Message - Publish(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) -} - -type clientClient struct { - cc *grpc.ClientConn -} - -func NewClientClient(cc *grpc.ClientConn) ClientClient { - return &clientClient{cc} -} - -func (c *clientClient) Call(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { - out := new(Response) - err := c.cc.Invoke(ctx, "/go.micro.client.Client/Call", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *clientClient) Stream(ctx context.Context, opts ...grpc.CallOption) (Client_StreamClient, error) { - stream, err := c.cc.NewStream(ctx, &_Client_serviceDesc.Streams[0], "/go.micro.client.Client/Stream", opts...) - if err != nil { - return nil, err - } - x := &clientStreamClient{stream} - return x, nil -} - -type Client_StreamClient interface { - Send(*Request) error - Recv() (*Response, error) - grpc.ClientStream -} - -type clientStreamClient struct { - grpc.ClientStream -} - -func (x *clientStreamClient) Send(m *Request) error { - return x.ClientStream.SendMsg(m) -} - -func (x *clientStreamClient) Recv() (*Response, error) { - m := new(Response) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *clientClient) Publish(ctx context.Context, in *Message, opts ...grpc.CallOption) (*Message, error) { - out := new(Message) - err := c.cc.Invoke(ctx, "/go.micro.client.Client/Publish", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// ClientServer is the server API for Client service. -type ClientServer interface { - // Call allows a single request to be made - Call(context.Context, *Request) (*Response, error) - // Stream is a bidirectional stream - Stream(Client_StreamServer) error - // Publish publishes a message and returns an empty Message - Publish(context.Context, *Message) (*Message, error) -} - -func RegisterClientServer(s *grpc.Server, srv ClientServer) { - s.RegisterService(&_Client_serviceDesc, srv) -} - -func _Client_Call_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Request) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ClientServer).Call(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.client.Client/Call", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ClientServer).Call(ctx, req.(*Request)) - } - return interceptor(ctx, in, info, handler) -} - -func _Client_Stream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(ClientServer).Stream(&clientStreamServer{stream}) -} - -type Client_StreamServer interface { - Send(*Response) error - Recv() (*Request, error) - grpc.ServerStream -} - -type clientStreamServer struct { - grpc.ServerStream -} - -func (x *clientStreamServer) Send(m *Response) error { - return x.ServerStream.SendMsg(m) -} - -func (x *clientStreamServer) Recv() (*Request, error) { - m := new(Request) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func _Client_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Message) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ClientServer).Publish(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.client.Client/Publish", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ClientServer).Publish(ctx, req.(*Message)) - } - return interceptor(ctx, in, info, handler) -} - -var _Client_serviceDesc = grpc.ServiceDesc{ - ServiceName: "go.micro.client.Client", - HandlerType: (*ClientServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Call", - Handler: _Client_Call_Handler, - }, - { - MethodName: "Publish", - Handler: _Client_Publish_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "Stream", - Handler: _Client_Stream_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, - Metadata: "micro/go-micro/client/proto/client.proto", +var fileDescriptor_d418333f021a3308 = []byte{ + // 279 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x91, 0x31, 0x4f, 0xfb, 0x30, + 0x10, 0xc5, 0xeb, 0xb6, 0xff, 0xa4, 0xff, 0xa3, 0x12, 0x92, 0xc5, 0x60, 0x32, 0xa0, 0x92, 0x29, + 0x0b, 0x0e, 0xa2, 0x33, 0x62, 0xc8, 0x8c, 0x84, 0x02, 0x62, 0x45, 0x89, 0x7b, 0x4a, 0x2d, 0x25, + 0xb6, 0x89, 0x9d, 0x4a, 0xf9, 0x90, 0x7c, 0x27, 0x24, 0x27, 0xa5, 0x08, 0xda, 0x85, 0xed, 0xde, + 0xfd, 0xac, 0x77, 0xe7, 0x77, 0xb0, 0xae, 0xa4, 0xdb, 0x76, 0x25, 0x17, 0xba, 0x49, 0x1b, 0x29, + 0x5a, 0x9d, 0x56, 0xfa, 0x66, 0x28, 0x44, 0x2d, 0x51, 0xb9, 0xd4, 0xb4, 0xda, 0xed, 0x05, 0xf7, + 0x82, 0x9e, 0x57, 0x9a, 0xfb, 0x37, 0x7c, 0x68, 0xc7, 0x3b, 0x08, 0x73, 0x7c, 0xef, 0xd0, 0x3a, + 0xca, 0x20, 0xb4, 0xd8, 0xee, 0xa4, 0x40, 0x46, 0x56, 0x24, 0xf9, 0x9f, 0xef, 0x25, 0x8d, 0x60, + 0x81, 0x6a, 0x63, 0xb4, 0x54, 0x8e, 0x4d, 0x3d, 0xfa, 0xd2, 0xf4, 0x1a, 0x96, 0x42, 0x2b, 0x87, + 0xca, 0xbd, 0xb9, 0xde, 0x20, 0x9b, 0x79, 0x7e, 0x36, 0xf6, 0x5e, 0x7a, 0x83, 0x94, 0xc2, 0xbc, + 0xd4, 0x9b, 0x9e, 0xcd, 0x57, 0x24, 0x59, 0xe6, 0xbe, 0x8e, 0xaf, 0x60, 0x91, 0xa3, 0x35, 0x5a, + 0xd9, 0x03, 0x27, 0xdf, 0xf8, 0x2b, 0x84, 0x8f, 0x68, 0x6d, 0x51, 0x21, 0xbd, 0x80, 0x7f, 0x4e, + 0x1b, 0x29, 0xc6, 0xad, 0x06, 0xf1, 0x6b, 0xee, 0xf4, 0xf4, 0xdc, 0xd9, 0xc1, 0xf7, 0xee, 0x83, + 0x40, 0x90, 0xf9, 0xaf, 0xd3, 0x7b, 0x98, 0x67, 0x45, 0x5d, 0x53, 0xc6, 0x7f, 0x84, 0xc2, 0xc7, + 0x44, 0xa2, 0xcb, 0x23, 0x64, 0xd8, 0x39, 0x9e, 0xd0, 0x0c, 0x82, 0x67, 0xd7, 0x62, 0xd1, 0xfc, + 0xd1, 0x20, 0x21, 0xb7, 0x84, 0x3e, 0x40, 0xf8, 0xd4, 0x95, 0xb5, 0xb4, 0xdb, 0x23, 0x2e, 0x63, + 0x00, 0xd1, 0x49, 0x12, 0x4f, 0xca, 0xc0, 0xdf, 0x75, 0xfd, 0x19, 0x00, 0x00, 0xff, 0xff, 0xb6, + 0x4d, 0x6e, 0xd5, 0x0e, 0x02, 0x00, 0x00, } diff --git a/client/proto/client.micro.go b/client/proto/client.pb.micro.go similarity index 98% rename from client/proto/client.micro.go rename to client/proto/client.pb.micro.go index 6fc4886e..140d9b45 100644 --- a/client/proto/client.micro.go +++ b/client/proto/client.pb.micro.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: micro/go-micro/client/proto/client.proto +// source: github.com/micro/go-micro/client/proto/client.proto package go_micro_client diff --git a/client/rpc_client.go b/client/rpc_client.go index 0a5a0580..4da6df45 100644 --- a/client/rpc_client.go +++ b/client/rpc_client.go @@ -13,6 +13,7 @@ import ( "github.com/micro/go-micro/client/pool" "github.com/micro/go-micro/client/selector" "github.com/micro/go-micro/codec" + raw "github.com/micro/go-micro/codec/bytes" "github.com/micro/go-micro/errors" "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/registry" @@ -161,7 +162,6 @@ func (r *rpcClient) call(ctx context.Context, node *registry.Node, req Request, select { case err := <-ch: - grr = err return err case <-ctx.Done(): grr = errors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err())) @@ -316,6 +316,22 @@ func (r *rpcClient) Options() Options { return r.opts } +// hasProxy checks if we have proxy set in the environment +func (r *rpcClient) hasProxy() bool { + // get proxy + if prx := os.Getenv("MICRO_PROXY"); len(prx) > 0 { + return true + } + + // get proxy address + if prx := os.Getenv("MICRO_PROXY_ADDRESS"); len(prx) > 0 { + return true + } + + return false +} + +// next returns an iterator for the next nodes to call func (r *rpcClient) next(request Request, opts CallOptions) (selector.Next, error) { service := request.Service() @@ -377,7 +393,9 @@ func (r *rpcClient) Call(ctx context.Context, request Request, response interfac d, ok := ctx.Deadline() if !ok { // no deadline so we create a new one - ctx, _ = context.WithTimeout(ctx, callOpts.RequestTimeout) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout) + defer cancel() } else { // got a deadline so no need to setup context // but we need to set the timeout we pass along @@ -429,10 +447,18 @@ func (r *rpcClient) Call(ctx context.Context, request Request, response interfac return err } - ch := make(chan error, callOpts.Retries+1) + // get the retries + retries := callOpts.Retries + + // disable retries when using a proxy + if r.hasProxy() { + retries = 0 + } + + ch := make(chan error, retries+1) var gerr error - for i := 0; i <= callOpts.Retries; i++ { + for i := 0; i <= retries; i++ { go func(i int) { ch <- call(i) }(i) @@ -512,10 +538,18 @@ func (r *rpcClient) Stream(ctx context.Context, request Request, opts ...CallOpt err error } - ch := make(chan response, callOpts.Retries+1) + // get the retries + retries := callOpts.Retries + + // disable retries when using a proxy + if r.hasProxy() { + retries = 0 + } + + ch := make(chan response, retries+1) var grr error - for i := 0; i <= callOpts.Retries; i++ { + for i := 0; i <= retries; i++ { go func(i int) { s, err := call(i) ch <- response{s, err} @@ -583,26 +617,37 @@ func (r *rpcClient) Publish(ctx context.Context, msg Message, opts ...PublishOpt return errors.InternalServerError("go.micro.client", err.Error()) } - // new buffer - b := buf.New(nil) + var body []byte - if err := cf(b).Write(&codec.Message{ - Target: topic, - Type: codec.Event, - Header: map[string]string{ - "Micro-Id": id, - "Micro-Topic": msg.Topic(), - }, - }, msg.Payload()); err != nil { - return errors.InternalServerError("go.micro.client", err.Error()) + // passed in raw data + if d, ok := msg.Payload().(*raw.Frame); ok { + body = d.Data + } else { + // new buffer + b := buf.New(nil) + + if err := cf(b).Write(&codec.Message{ + Target: topic, + Type: codec.Event, + Header: map[string]string{ + "Micro-Id": id, + "Micro-Topic": msg.Topic(), + }, + }, msg.Payload()); err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + + // set the body + body = b.Bytes() } + r.once.Do(func() { r.opts.Broker.Connect() }) return r.opts.Broker.Publish(topic, &broker.Message{ Header: md, - Body: b.Bytes(), + Body: body, }) } diff --git a/client/rpc_client_test.go b/client/rpc_client_test.go index 14547e9f..ace08fff 100644 --- a/client/rpc_client_test.go +++ b/client/rpc_client_test.go @@ -12,10 +12,7 @@ import ( ) func newTestRegistry() registry.Registry { - r := memory.NewRegistry() - reg := r.(*memory.Registry) - reg.Services = testData - return reg + return memory.NewRegistry(memory.Services(testData)) } func TestCallAddress(t *testing.T) { @@ -143,9 +140,12 @@ func TestCallWrapper(t *testing.T) { Name: service, Version: "latest", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: id, Address: address, + Metadata: map[string]string{ + "protocol": "mucp", + }, }, }, }) diff --git a/client/rpc_codec.go b/client/rpc_codec.go index c20537ea..a71f6a11 100644 --- a/client/rpc_codec.go +++ b/client/rpc_codec.go @@ -88,32 +88,24 @@ func (rwc *readWriteCloser) Close() error { } func getHeaders(m *codec.Message) { - get := func(hdr string) string { - if hd := m.Header[hdr]; len(hd) > 0 { - return hd + set := func(v, hdr string) string { + if len(v) > 0 { + return v } - // old - return m.Header["X-"+hdr] + return m.Header[hdr] } // check error in header - if len(m.Error) == 0 { - m.Error = get("Micro-Error") - } + m.Error = set(m.Error, "Micro-Error") // check endpoint in header - if len(m.Endpoint) == 0 { - m.Endpoint = get("Micro-Endpoint") - } + m.Endpoint = set(m.Endpoint, "Micro-Endpoint") // check method in header - if len(m.Method) == 0 { - m.Method = get("Micro-Method") - } + m.Method = set(m.Method, "Micro-Method") - if len(m.Id) == 0 { - m.Id = get("Micro-Id") - } + // set the request id + m.Id = set(m.Id, "Micro-Id") } func setHeaders(m *codec.Message, stream string) { @@ -122,7 +114,6 @@ func setHeaders(m *codec.Message, stream string) { return } m.Header[hdr] = v - m.Header["X-"+hdr] = v } set("Micro-Id", m.Id) @@ -145,6 +136,11 @@ func setupProtocol(msg *transport.Message, node *registry.Node) codec.NewCodec { return nil } + // processing topic publishing + if len(msg.Header["Micro-Topic"]) > 0 { + return nil + } + // no protocol use old codecs switch msg.Header["Content-Type"] { case "application/json": diff --git a/client/selector/common_test.go b/client/selector/common_test.go index 7aba0542..1af55c1c 100644 --- a/client/selector/common_test.go +++ b/client/selector/common_test.go @@ -7,7 +7,7 @@ import ( var ( // mock data testData = map[string][]*registry.Service{ - "foo": []*registry.Service{ + "foo": { { Name: "foo", Version: "1.0.0", diff --git a/client/selector/default_test.go b/client/selector/default_test.go index 77075204..948eedcd 100644 --- a/client/selector/default_test.go +++ b/client/selector/default_test.go @@ -9,9 +9,7 @@ import ( func TestRegistrySelector(t *testing.T) { counts := map[string]int{} - r := memory.NewRegistry() - rg := r.(*memory.Registry) - rg.Services = testData + r := memory.NewRegistry(memory.Services(testData)) cache := NewSelector(Registry(r)) next, err := cache.Select("foo") diff --git a/client/selector/dns/dns.go b/client/selector/dns/dns.go index df6c209f..450ae412 100644 --- a/client/selector/dns/dns.go +++ b/client/selector/dns/dns.go @@ -63,7 +63,7 @@ func (d *dnsSelector) Select(service string, opts ...selector.SelectOption) (sel } } - var nodes []*registry.Node + nodes := make([]*registry.Node, 0, len(srv)) for _, node := range srv { nodes = append(nodes, ®istry.Node{ Id: node.Target, @@ -72,7 +72,7 @@ func (d *dnsSelector) Select(service string, opts ...selector.SelectOption) (sel } services := []*registry.Service{ - ®istry.Service{ + { Name: service, Nodes: nodes, }, @@ -99,13 +99,9 @@ func (d *dnsSelector) Select(service string, opts ...selector.SelectOption) (sel return sopts.Strategy(services), nil } -func (d *dnsSelector) Mark(service string, node *registry.Node, err error) { - return -} +func (d *dnsSelector) Mark(service string, node *registry.Node, err error) {} -func (d *dnsSelector) Reset(service string) { - return -} +func (d *dnsSelector) Reset(service string) {} func (d *dnsSelector) Close() error { return nil diff --git a/client/selector/filter_test.go b/client/selector/filter_test.go index 5a3e9c0d..035feed3 100644 --- a/client/selector/filter_test.go +++ b/client/selector/filter_test.go @@ -14,20 +14,20 @@ func TestFilterEndpoint(t *testing.T) { }{ { services: []*registry.Service{ - ®istry.Service{ + { Name: "test", Version: "1.0.0", Endpoints: []*registry.Endpoint{ - ®istry.Endpoint{ + { Name: "Foo.Bar", }, }, }, - ®istry.Service{ + { Name: "test", Version: "1.1.0", Endpoints: []*registry.Endpoint{ - ®istry.Endpoint{ + { Name: "Baz.Bar", }, }, @@ -38,20 +38,20 @@ func TestFilterEndpoint(t *testing.T) { }, { services: []*registry.Service{ - ®istry.Service{ + { Name: "test", Version: "1.0.0", Endpoints: []*registry.Endpoint{ - ®istry.Endpoint{ + { Name: "Foo.Bar", }, }, }, - ®istry.Service{ + { Name: "test", Version: "1.1.0", Endpoints: []*registry.Endpoint{ - ®istry.Endpoint{ + { Name: "Foo.Bar", }, }, @@ -95,11 +95,11 @@ func TestFilterLabel(t *testing.T) { }{ { services: []*registry.Service{ - ®istry.Service{ + { Name: "test", Version: "1.0.0", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test-1", Address: "localhost", Metadata: map[string]string{ @@ -108,11 +108,11 @@ func TestFilterLabel(t *testing.T) { }, }, }, - ®istry.Service{ + { Name: "test", Version: "1.1.0", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test-2", Address: "localhost", Metadata: map[string]string{ @@ -127,21 +127,21 @@ func TestFilterLabel(t *testing.T) { }, { services: []*registry.Service{ - ®istry.Service{ + { Name: "test", Version: "1.0.0", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test-1", Address: "localhost", }, }, }, - ®istry.Service{ + { Name: "test", Version: "1.1.0", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test-2", Address: "localhost", }, @@ -187,11 +187,11 @@ func TestFilterVersion(t *testing.T) { }{ { services: []*registry.Service{ - ®istry.Service{ + { Name: "test", Version: "1.0.0", }, - ®istry.Service{ + { Name: "test", Version: "1.1.0", }, @@ -201,11 +201,11 @@ func TestFilterVersion(t *testing.T) { }, { services: []*registry.Service{ - ®istry.Service{ + { Name: "test", Version: "1.0.0", }, - ®istry.Service{ + { Name: "test", Version: "1.1.0", }, diff --git a/client/selector/router/router.go b/client/selector/router/router.go index 90f74a9a..d8e8aae3 100644 --- a/client/selector/router/router.go +++ b/client/selector/router/router.go @@ -11,7 +11,7 @@ import ( "github.com/micro/go-micro/client/selector" "github.com/micro/go-micro/registry" "github.com/micro/go-micro/router" - pb "github.com/micro/go-micro/router/proto" + pb "github.com/micro/go-micro/router/service/proto" ) type routerSelector struct { @@ -43,9 +43,9 @@ type routerKey struct{} func (r *routerSelector) getRoutes(service string) ([]router.Route, error) { if !r.remote { // lookup router for routes for the service - return r.r.Lookup(router.NewQuery( + return r.r.Lookup( router.QueryService(service), - )) + ) } // lookup the remote router @@ -101,7 +101,7 @@ func (r *routerSelector) getRoutes(service string) ([]router.Route, error) { return nil, selector.ErrNoneAvailable } - var routes []router.Route + routes := make([]router.Route, 0, len(pbRoutes.Routes)) // convert from pb to []*router.Route for _, r := range pbRoutes.Routes { @@ -111,7 +111,7 @@ func (r *routerSelector) getRoutes(service string) ([]router.Route, error) { Gateway: r.Gateway, Network: r.Network, Link: r.Link, - Metric: int(r.Metric), + Metric: r.Metric, }) } @@ -176,12 +176,10 @@ func (r *routerSelector) Select(service string, opts ...selector.SelectOption) ( func (r *routerSelector) Mark(service string, node *registry.Node, err error) { // TODO: pass back metrics or information to the router - return } func (r *routerSelector) Reset(service string) { // TODO: reset the metrics or information at the router - return } func (r *routerSelector) Close() error { diff --git a/client/selector/selector.go b/client/selector/selector.go index 5c9a2499..83d82974 100644 --- a/client/selector/selector.go +++ b/client/selector/selector.go @@ -3,6 +3,7 @@ package selector import ( "errors" + "github.com/micro/go-micro/registry" ) diff --git a/client/selector/strategy.go b/client/selector/strategy.go index 9455d3f3..5014d25d 100644 --- a/client/selector/strategy.go +++ b/client/selector/strategy.go @@ -14,7 +14,7 @@ func init() { // Random is a random strategy algorithm for node selection func Random(services []*registry.Service) Next { - var nodes []*registry.Node + nodes := make([]*registry.Node, 0, len(services)) for _, service := range services { nodes = append(nodes, service.Nodes...) @@ -32,7 +32,7 @@ func Random(services []*registry.Service) Next { // RoundRobin is a roundrobin strategy algorithm for node selection func RoundRobin(services []*registry.Service) Next { - var nodes []*registry.Node + nodes := make([]*registry.Node, 0, len(services)) for _, service := range services { nodes = append(nodes, service.Nodes...) diff --git a/client/selector/strategy_test.go b/client/selector/strategy_test.go index 8ea9376a..2529a6b7 100644 --- a/client/selector/strategy_test.go +++ b/client/selector/strategy_test.go @@ -8,29 +8,29 @@ import ( func TestStrategies(t *testing.T) { testData := []*registry.Service{ - ®istry.Service{ + { Name: "test1", Version: "latest", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test1-1", Address: "10.0.0.1:1001", }, - ®istry.Node{ + { Id: "test1-2", Address: "10.0.0.2:1002", }, }, }, - ®istry.Service{ + { Name: "test1", Version: "default", Nodes: []*registry.Node{ - ®istry.Node{ + { Id: "test1-3", Address: "10.0.0.3:1003", }, - ®istry.Node{ + { Id: "test1-4", Address: "10.0.0.4:1004", }, diff --git a/codec/bytes/bytes.go b/codec/bytes/bytes.go index 67ff4038..69d36235 100644 --- a/codec/bytes/bytes.go +++ b/codec/bytes/bytes.go @@ -29,12 +29,10 @@ func (c *Codec) ReadBody(b interface{}) error { return err } - switch b.(type) { + switch v := 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) @@ -45,14 +43,13 @@ func (c *Codec) ReadBody(b interface{}) error { func (c *Codec) Write(m *codec.Message, b interface{}) error { var v []byte - switch b.(type) { + switch vb := b.(type) { case *Frame: - v = b.(*Frame).Data + v = vb.Data case *[]byte: - ve := b.(*[]byte) - v = *ve + v = *vb case []byte: - v = b.([]byte) + v = vb default: return fmt.Errorf("failed to write: %v is not type of *[]byte or []byte", b) } diff --git a/codec/bytes/marshaler.go b/codec/bytes/marshaler.go index 76599b65..86af8984 100644 --- a/codec/bytes/marshaler.go +++ b/codec/bytes/marshaler.go @@ -12,25 +12,22 @@ type Message struct { } func (n Marshaler) Marshal(v interface{}) ([]byte, error) { - switch v.(type) { + switch ve := v.(type) { case *[]byte: - ve := v.(*[]byte) return *ve, nil case []byte: - return v.([]byte), nil + return ve, nil case *Message: - return v.(*Message).Body, nil + return ve.Body, nil } return nil, errors.New("invalid message") } func (n Marshaler) Unmarshal(d []byte, v interface{}) error { - switch v.(type) { + switch ve := v.(type) { case *[]byte: - ve := v.(*[]byte) *ve = d case *Message: - ve := v.(*Message) ve.Body = d } return errors.New("invalid message") diff --git a/codec/jsonrpc/jsonrpc.go b/codec/jsonrpc/jsonrpc.go index f98b2842..d7e0e1c9 100644 --- a/codec/jsonrpc/jsonrpc.go +++ b/codec/jsonrpc/jsonrpc.go @@ -60,7 +60,6 @@ func (j *jsonCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error { default: return fmt.Errorf("Unrecognised message type: %v", mt) } - return nil } func (j *jsonCodec) ReadBody(b interface{}) error { diff --git a/codec/text/text.go b/codec/text/text.go index da43da50..799cf6c1 100644 --- a/codec/text/text.go +++ b/codec/text/text.go @@ -29,15 +29,12 @@ func (c *Codec) ReadBody(b interface{}) error { return err } - switch b.(type) { + switch v := b.(type) { case *string: - v := b.(*string) *v = string(buf) 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) @@ -48,20 +45,17 @@ func (c *Codec) ReadBody(b interface{}) error { func (c *Codec) Write(m *codec.Message, b interface{}) error { var v []byte - switch b.(type) { + switch ve := b.(type) { case *Frame: - v = b.(*Frame).Data + v = ve.Data case *[]byte: - ve := b.(*[]byte) v = *ve case *string: - ve := b.(*string) v = []byte(*ve) case string: - ve := b.(string) v = []byte(ve) case []byte: - v = b.([]byte) + v = ve default: return fmt.Errorf("failed to write: %v is not type of *[]byte or []byte", b) } diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index d6ee3eb2..f436ab18 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -23,11 +23,11 @@ import ( "github.com/micro/go-micro/broker/http" "github.com/micro/go-micro/broker/memory" "github.com/micro/go-micro/broker/nats" + brokerSrv "github.com/micro/go-micro/broker/service" // 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/etcd" "github.com/micro/go-micro/registry/mdns" rmem "github.com/micro/go-micro/registry/memory" regSrv "github.com/micro/go-micro/registry/service" @@ -44,6 +44,10 @@ import ( thttp "github.com/micro/go-micro/transport/http" tmem "github.com/micro/go-micro/transport/memory" "github.com/micro/go-micro/transport/quic" + + // runtimes + "github.com/micro/go-micro/runtime" + "github.com/micro/go-micro/runtime/kubernetes" ) type Cmd interface { @@ -151,16 +155,27 @@ var ( EnvVar: "MICRO_BROKER_ADDRESS", Usage: "Comma-separated list of broker addresses", }, + cli.StringFlag{ + Name: "profile", + Usage: "Debug profiler for cpu and memory stats", + EnvVar: "MICRO_DEBUG_PROFILE", + }, cli.StringFlag{ Name: "registry", EnvVar: "MICRO_REGISTRY", - Usage: "Registry for discovery. consul, mdns", + Usage: "Registry for discovery. etcd, mdns", }, cli.StringFlag{ Name: "registry_address", EnvVar: "MICRO_REGISTRY_ADDRESS", Usage: "Comma-separated list of registry addresses", }, + cli.StringFlag{ + Name: "runtime", + Usage: "Runtime for building and running services e.g local, kubernetes", + EnvVar: "MICRO_RUNTIME", + Value: "local", + }, cli.StringFlag{ Name: "selector", EnvVar: "MICRO_SELECTOR", @@ -179,9 +194,10 @@ var ( } DefaultBrokers = map[string]func(...broker.Option) broker.Broker{ - "http": http.NewBroker, - "memory": memory.NewBroker, - "nats": nats.NewBroker, + "service": brokerSrv.NewBroker, + "http": http.NewBroker, + "memory": memory.NewBroker, + "nats": nats.NewBroker, } DefaultClients = map[string]func(...client.Option) client.Client{ @@ -191,12 +207,10 @@ var ( } DefaultRegistries = map[string]func(...registry.Option) registry.Registry{ - "go.micro.registry": regSrv.NewRegistry, - "service": regSrv.NewRegistry, - "consul": consul.NewRegistry, - "gossip": gossip.NewRegistry, - "mdns": mdns.NewRegistry, - "memory": rmem.NewRegistry, + "service": regSrv.NewRegistry, + "etcd": etcd.NewRegistry, + "mdns": mdns.NewRegistry, + "memory": rmem.NewRegistry, } DefaultSelectors = map[string]func(...selector.Option) selector.Selector{ @@ -220,6 +234,11 @@ var ( "quic": quic.NewTransport, } + DefaultRuntimes = map[string]func(...runtime.Option) runtime.Runtime{ + "local": runtime.NewRuntime, + "kubernetes": kubernetes.NewRuntime, + } + // used for default selection as the fall back defaultClient = "rpc" defaultServer = "rpc" @@ -227,6 +246,7 @@ var ( defaultRegistry = "mdns" defaultSelector = "registry" defaultTransport = "http" + defaultRuntime = "local" ) func init() { @@ -246,6 +266,7 @@ func newCmd(opts ...Option) Cmd { Server: &server.DefaultServer, Selector: &selector.DefaultSelector, Transport: &transport.DefaultTransport, + Runtime: &runtime.DefaultRuntime, Brokers: DefaultBrokers, Clients: DefaultClients, @@ -253,6 +274,7 @@ func newCmd(opts ...Option) Cmd { Selectors: DefaultSelectors, Servers: DefaultServers, Transports: DefaultTransports, + Runtimes: DefaultRuntimes, } for _, o := range opts { @@ -293,6 +315,16 @@ func (c *cmd) Before(ctx *cli.Context) error { var serverOpts []server.Option var clientOpts []client.Option + // Set the runtime + if name := ctx.String("runtime"); len(name) > 0 { + r, ok := c.opts.Runtimes[name] + if !ok { + return fmt.Errorf("Unsupported runtime: %s", name) + } + + *c.opts.Runtime = r() + } + // Set the client if name := ctx.String("client"); len(name) > 0 { // only change if we have the client and type differs diff --git a/config/cmd/options.go b/config/cmd/options.go index 1f221f35..be831fe1 100644 --- a/config/cmd/options.go +++ b/config/cmd/options.go @@ -7,6 +7,7 @@ import ( "github.com/micro/go-micro/client" "github.com/micro/go-micro/client/selector" "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/runtime" "github.com/micro/go-micro/server" "github.com/micro/go-micro/transport" ) @@ -24,6 +25,7 @@ type Options struct { Transport *transport.Transport Client *client.Client Server *server.Server + Runtime *runtime.Runtime Brokers map[string]func(...broker.Option) broker.Broker Clients map[string]func(...client.Option) client.Client @@ -31,6 +33,7 @@ type Options struct { Selectors map[string]func(...selector.Option) selector.Selector Servers map[string]func(...server.Option) server.Server Transports map[string]func(...transport.Option) transport.Transport + Runtimes map[string]func(...runtime.Option) runtime.Runtime // Other options for implementations of the interface // can be stored in a context @@ -135,3 +138,10 @@ func NewTransport(name string, t func(...transport.Option) transport.Transport) o.Transports[name] = t } } + +// New runtime func +func NewRuntime(name string, r func(...runtime.Option) runtime.Runtime) Option { + return func(o *Options) { + o.Runtimes[name] = r + } +} diff --git a/config/loader/memory/memory.go b/config/loader/memory/memory.go index 6d5b27cb..60af473a 100644 --- a/config/loader/memory/memory.go +++ b/config/loader/memory/memory.go @@ -2,6 +2,7 @@ package memory import ( "bytes" + "container/list" "errors" "fmt" "strings" @@ -28,8 +29,7 @@ type memory struct { // all the sources sources []source.Source - idx int - watchers map[int]*watcher + watchers *list.List } type watcher struct { @@ -153,11 +153,11 @@ func (m *memory) reload() error { } func (m *memory) update() { - var watchers []*watcher + watchers := make([]*watcher, 0, m.watchers.Len()) m.RLock() - for _, w := range m.watchers { - watchers = append(watchers, w) + for e := m.watchers.Front(); e != nil; e = e.Next() { + watchers = append(watchers, e.Value.(*watcher)) } m.RUnlock() @@ -193,6 +193,7 @@ func (m *memory) Snapshot() (*loader.Snapshot, error) { // Sync loads all the sources, calls the parser and updates the config func (m *memory) Sync() error { + //nolint:prealloc var sets []*source.ChangeSet m.Lock() @@ -335,16 +336,14 @@ func (m *memory) Watch(path ...string) (loader.Watcher, error) { updates: make(chan reader.Value, 1), } - id := m.idx - m.watchers[id] = w - m.idx++ + e := m.watchers.PushBack(w) m.Unlock() go func() { <-w.exit m.Lock() - delete(m.watchers, id) + m.watchers.Remove(e) m.Unlock() }() @@ -403,7 +402,7 @@ func NewLoader(opts ...loader.Option) loader.Loader { m := &memory{ exit: make(chan bool), opts: options, - watchers: make(map[int]*watcher), + watchers: list.New(), sources: options.Source, } diff --git a/config/options/options.go b/config/options/options.go index 588f4838..b465ba63 100644 --- a/config/options/options.go +++ b/config/options/options.go @@ -2,6 +2,7 @@ package options import ( + "log" "sync" ) @@ -68,6 +69,8 @@ func WithString(s string) Option { // NewOptions returns a new initialiser func NewOptions(opts ...Option) Options { o := new(defaultOptions) - o.Init(opts...) + if err := o.Init(opts...); err != nil { + log.Fatal(err) + } return o } diff --git a/config/reader/json/values_test.go b/config/reader/json/values_test.go index 97aec68a..166d9ae0 100644 --- a/config/reader/json/values_test.go +++ b/config/reader/json/values_test.go @@ -62,7 +62,7 @@ func TestStructArray(t *testing.T) { { []byte(`[{"foo": "bar"}]`), emptyTSlice, - []T{T{Foo: "bar"}}, + []T{{Foo: "bar"}}, }, } diff --git a/config/source/consul/README.md b/config/source/consul/README.md deleted file mode 100644 index 28006fc4..00000000 --- a/config/source/consul/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Consul Source - -The consul source reads config from consul key/values - -## Consul Format - -The consul source expects keys under the default prefix `/micro/config` - -Values are expected to be json - -``` -// set database -consul kv put micro/config/database '{"address": "10.0.0.1", "port": 3306}' -// set cache -consul kv put micro/config/cache '{"address": "10.0.0.2", "port": 6379}' -``` - -Keys are split on `/` so access becomes - -``` -conf.Get("micro", "config", "database") -``` - -## New Source - -Specify source with data - -```go -consulSource := consul.NewSource( - // optionally specify consul address; default to localhost:8500 - consul.WithAddress("10.0.0.10:8500"), - // optionally specify prefix; defaults to /micro/config - consul.WithPrefix("/my/prefix"), - // optionally strip the provided prefix from the keys, defaults to false - consul.StripPrefix(true), -) -``` - -## Load Source - -Load the source into config - -```go -// Create new config -conf := config.NewConfig() - -// Load consul source -conf.Load(consulSource) -``` diff --git a/config/source/consul/consul.go b/config/source/consul/consul.go deleted file mode 100644 index f5c3c695..00000000 --- a/config/source/consul/consul.go +++ /dev/null @@ -1,126 +0,0 @@ -package consul - -import ( - "fmt" - "net" - "time" - - "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/config/source" -) - -// Currently a single consul reader -type consul struct { - prefix string - stripPrefix string - addr string - opts source.Options - client *api.Client -} - -var ( - // DefaultPrefix is the prefix that consul keys will be assumed to have if you - // haven't specified one - DefaultPrefix = "/micro/config/" -) - -func (c *consul) Read() (*source.ChangeSet, error) { - kv, _, err := c.client.KV().List(c.prefix, nil) - if err != nil { - return nil, err - } - - if kv == nil || len(kv) == 0 { - return nil, fmt.Errorf("source not found: %s", c.prefix) - } - - data, err := makeMap(c.opts.Encoder, kv, c.stripPrefix) - if err != nil { - return nil, fmt.Errorf("error reading data: %v", err) - } - - b, err := c.opts.Encoder.Encode(data) - if err != nil { - return nil, fmt.Errorf("error reading source: %v", err) - } - - cs := &source.ChangeSet{ - Timestamp: time.Now(), - Format: c.opts.Encoder.String(), - Source: c.String(), - Data: b, - } - cs.Checksum = cs.Sum() - - return cs, nil -} - -func (c *consul) String() string { - return "consul" -} - -func (c *consul) Watch() (source.Watcher, error) { - w, err := newWatcher(c.prefix, c.addr, c.String(), c.stripPrefix, c.opts.Encoder) - if err != nil { - return nil, err - } - return w, nil -} - -// NewSource creates a new consul source -func NewSource(opts ...source.Option) source.Source { - options := source.NewOptions(opts...) - - // use default config - config := api.DefaultConfig() - - // use the consul config passed in the options if any - if co, ok := options.Context.Value(configKey{}).(*api.Config); ok { - config = co - } - - // check if there are any addrs - a, ok := options.Context.Value(addressKey{}).(string) - if ok { - addr, port, err := net.SplitHostPort(a) - if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { - port = "8500" - addr = a - config.Address = fmt.Sprintf("%s:%s", addr, port) - } else if err == nil { - config.Address = fmt.Sprintf("%s:%s", addr, port) - } - } - - dc, ok := options.Context.Value(dcKey{}).(string) - if ok { - config.Datacenter = dc - } - - token, ok := options.Context.Value(tokenKey{}).(string) - if ok { - config.Token = token - } - - // create the client - client, _ := api.NewClient(config) - - prefix := DefaultPrefix - sp := "" - f, ok := options.Context.Value(prefixKey{}).(string) - if ok { - prefix = f - } - - if b, ok := options.Context.Value(stripPrefixKey{}).(bool); ok && b { - sp = prefix - } - - return &consul{ - prefix: prefix, - stripPrefix: sp, - addr: config.Address, - opts: options, - client: client, - } -} diff --git a/config/source/consul/util.go b/config/source/consul/util.go deleted file mode 100644 index 1deacbb4..00000000 --- a/config/source/consul/util.go +++ /dev/null @@ -1,89 +0,0 @@ -package consul - -import ( - "fmt" - "strings" - - "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/config/encoder" -) - -type configValue interface { - Value() interface{} - Decode(encoder.Encoder, []byte) error -} -type configArrayValue struct { - v []interface{} -} - -func (a *configArrayValue) Value() interface{} { return a.v } -func (a *configArrayValue) Decode(e encoder.Encoder, b []byte) error { - return e.Decode(b, &a.v) -} - -type configMapValue struct { - v map[string]interface{} -} - -func (m *configMapValue) Value() interface{} { return m.v } -func (m *configMapValue) Decode(e encoder.Encoder, b []byte) error { - return e.Decode(b, &m.v) -} - -func makeMap(e encoder.Encoder, kv api.KVPairs, stripPrefix string) (map[string]interface{}, error) { - - data := make(map[string]interface{}) - - // consul guarantees lexicographic order, so no need to sort - for _, v := range kv { - pathString := strings.TrimPrefix(strings.TrimPrefix(v.Key, strings.TrimPrefix(stripPrefix, "/")), "/") - if pathString == "" { - continue - } - var val configValue - var err error - - // ensure a valid value is stored at this location - if len(v.Value) > 0 { - // try to decode into map value or array value - arrayV := &configArrayValue{v: []interface{}{}} - mapV := &configMapValue{v: map[string]interface{}{}} - switch { - case arrayV.Decode(e, v.Value) == nil: - val = arrayV - case mapV.Decode(e, v.Value) == nil: - val = mapV - default: - return nil, fmt.Errorf("faild decode value. path: %s, error: %s", pathString, err) - } - } - - // set target at the root - target := data - path := strings.Split(pathString, "/") - // find (or create) the leaf node we want to put this value at - for _, dir := range path[:len(path)-1] { - if _, ok := target[dir]; !ok { - target[dir] = make(map[string]interface{}) - } - target = target[dir].(map[string]interface{}) - } - - leafDir := path[len(path)-1] - - // copy over the keys from the value - switch val.(type) { - case *configArrayValue: - target[leafDir] = val.Value() - case *configMapValue: - target[leafDir] = make(map[string]interface{}) - target = target[leafDir].(map[string]interface{}) - mapv := val.Value().(map[string]interface{}) - for k := range mapv { - target[k] = mapv[k] - } - } - } - - return data, nil -} diff --git a/config/source/consul/watcher.go b/config/source/consul/watcher.go deleted file mode 100644 index a20c8f9b..00000000 --- a/config/source/consul/watcher.go +++ /dev/null @@ -1,95 +0,0 @@ -package consul - -import ( - "time" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/api/watch" - "github.com/micro/go-micro/config/encoder" - "github.com/micro/go-micro/config/source" -) - -type watcher struct { - e encoder.Encoder - name string - stripPrefix string - - wp *watch.Plan - ch chan *source.ChangeSet - exit chan bool -} - -func newWatcher(key, addr, name, stripPrefix string, e encoder.Encoder) (source.Watcher, error) { - w := &watcher{ - e: e, - name: name, - stripPrefix: stripPrefix, - ch: make(chan *source.ChangeSet), - exit: make(chan bool), - } - - wp, err := watch.Parse(map[string]interface{}{"type": "keyprefix", "prefix": key}) - if err != nil { - return nil, err - } - - wp.Handler = w.handle - - // wp.Run is a blocking call and will prevent newWatcher from returning - go wp.Run(addr) - - w.wp = wp - - return w, nil -} - -func (w *watcher) handle(idx uint64, data interface{}) { - if data == nil { - return - } - - kvs, ok := data.(api.KVPairs) - if !ok { - return - } - - d, err := makeMap(w.e, kvs, w.stripPrefix) - if err != nil { - return - } - - b, err := w.e.Encode(d) - if err != nil { - return - } - - cs := &source.ChangeSet{ - Timestamp: time.Now(), - Format: w.e.String(), - Source: w.name, - Data: b, - } - cs.Checksum = cs.Sum() - - w.ch <- cs -} - -func (w *watcher) Next() (*source.ChangeSet, error) { - select { - case cs := <-w.ch: - return cs, nil - case <-w.exit: - return nil, source.ErrWatcherStopped - } -} - -func (w *watcher) Stop() error { - select { - case <-w.exit: - return nil - default: - w.wp.Stop() - close(w.exit) - } - return nil -} diff --git a/config/source/env/options.go b/config/source/env/options.go index 112a7db2..495672b5 100644 --- a/config/source/env/options.go +++ b/config/source/env/options.go @@ -35,6 +35,7 @@ func WithPrefix(p ...string) source.Option { } func appendUnderscore(prefixes []string) []string { + //nolint:prealloc var result []string for _, p := range prefixes { if !strings.HasSuffix(p, "_") { diff --git a/config/source/etcd/README.md b/config/source/etcd/README.md new file mode 100644 index 00000000..a3025ad4 --- /dev/null +++ b/config/source/etcd/README.md @@ -0,0 +1,51 @@ +# Etcd Source + +The etcd source reads config from etcd key/values + +This source supports etcd version 3 and beyond. + +## Etcd Format + +The etcd source expects keys under the default prefix `/micro/config` (prefix can be changed) + +Values are expected to be JSON + +``` +// set database +etcdctl put /micro/config/database '{"address": "10.0.0.1", "port": 3306}' +// set cache +etcdctl put /micro/config/cache '{"address": "10.0.0.2", "port": 6379}' +``` + +Keys are split on `/` so access becomes + +``` +conf.Get("micro", "config", "database") +``` + +## New Source + +Specify source with data + +```go +etcdSource := etcd.NewSource( + // optionally specify etcd address; default to localhost:8500 + etcd.WithAddress("10.0.0.10:8500"), + // optionally specify prefix; defaults to /micro/config + etcd.WithPrefix("/my/prefix"), + // optionally strip the provided prefix from the keys, defaults to false + etcd.StripPrefix(true), +) +``` + +## Load Source + +Load the source into config + +```go +// Create new config +conf := config.NewConfig() + +// Load file source +conf.Load(etcdSource) +``` diff --git a/config/source/etcd/etcd.go b/config/source/etcd/etcd.go new file mode 100644 index 00000000..7c8edba9 --- /dev/null +++ b/config/source/etcd/etcd.go @@ -0,0 +1,141 @@ +package etcd + +import ( + "context" + "fmt" + "net" + "time" + + cetcd "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/mvcc/mvccpb" + "github.com/micro/go-micro/config/source" +) + +// Currently a single etcd reader +type etcd struct { + prefix string + stripPrefix string + opts source.Options + client *cetcd.Client + cerr error +} + +var ( + DefaultPrefix = "/micro/config/" +) + +func (c *etcd) Read() (*source.ChangeSet, error) { + if c.cerr != nil { + return nil, c.cerr + } + + rsp, err := c.client.Get(context.Background(), c.prefix, cetcd.WithPrefix()) + if err != nil { + return nil, err + } + + if rsp == nil || len(rsp.Kvs) == 0 { + return nil, fmt.Errorf("source not found: %s", c.prefix) + } + + kvs := make([]*mvccpb.KeyValue, 0, len(rsp.Kvs)) + for _, v := range rsp.Kvs { + kvs = append(kvs, (*mvccpb.KeyValue)(v)) + } + + data := makeMap(c.opts.Encoder, kvs, c.stripPrefix) + + b, err := c.opts.Encoder.Encode(data) + if err != nil { + return nil, fmt.Errorf("error reading source: %v", err) + } + + cs := &source.ChangeSet{ + Timestamp: time.Now(), + Source: c.String(), + Data: b, + Format: c.opts.Encoder.String(), + } + cs.Checksum = cs.Sum() + + return cs, nil +} + +func (c *etcd) String() string { + return "etcd" +} + +func (c *etcd) Watch() (source.Watcher, error) { + if c.cerr != nil { + return nil, c.cerr + } + cs, err := c.Read() + if err != nil { + return nil, err + } + return newWatcher(c.prefix, c.stripPrefix, c.client.Watcher, cs, c.opts) +} + +func NewSource(opts ...source.Option) source.Source { + options := source.NewOptions(opts...) + + var endpoints []string + + // check if there are any addrs + addrs, ok := options.Context.Value(addressKey{}).([]string) + if ok { + for _, a := range addrs { + addr, port, err := net.SplitHostPort(a) + if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { + port = "2379" + addr = a + endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) + } else if err == nil { + endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) + } + } + } + + if len(endpoints) == 0 { + endpoints = []string{"localhost:2379"} + } + + // check dial timeout option + dialTimeout, ok := options.Context.Value(dialTimeoutKey{}).(time.Duration) + if !ok { + dialTimeout = 3 * time.Second // default dial timeout + } + + config := cetcd.Config{ + Endpoints: endpoints, + DialTimeout: dialTimeout, + } + + u, ok := options.Context.Value(authKey{}).(*authCreds) + if ok { + config.Username = u.Username + config.Password = u.Password + } + + // use default config + client, err := cetcd.New(config) + + prefix := DefaultPrefix + sp := "" + f, ok := options.Context.Value(prefixKey{}).(string) + if ok { + prefix = f + } + + if b, ok := options.Context.Value(stripPrefixKey{}).(bool); ok && b { + sp = prefix + } + + return &etcd{ + prefix: prefix, + stripPrefix: sp, + opts: options, + client: client, + cerr: err, + } +} diff --git a/config/source/consul/options.go b/config/source/etcd/options.go similarity index 61% rename from config/source/consul/options.go rename to config/source/etcd/options.go index 8bca6a66..325d2f46 100644 --- a/config/source/consul/options.go +++ b/config/source/etcd/options.go @@ -1,21 +1,25 @@ -package consul +package etcd import ( "context" + "time" - "github.com/hashicorp/consul/api" "github.com/micro/go-micro/config/source" ) type addressKey struct{} type prefixKey struct{} type stripPrefixKey struct{} -type dcKey struct{} -type tokenKey struct{} -type configKey struct{} +type authKey struct{} +type dialTimeoutKey struct{} -// WithAddress sets the consul address -func WithAddress(a string) source.Option { +type authCreds struct { + Username string + Password string +} + +// WithAddress sets the etcd address +func WithAddress(a ...string) source.Option { return func(o *source.Options) { if o.Context == nil { o.Context = context.Background() @@ -45,31 +49,22 @@ func StripPrefix(strip bool) source.Option { } } -func WithDatacenter(p string) source.Option { +// Auth allows you to specify username/password +func Auth(username, password string) source.Option { return func(o *source.Options) { if o.Context == nil { o.Context = context.Background() } - o.Context = context.WithValue(o.Context, dcKey{}, p) + o.Context = context.WithValue(o.Context, authKey{}, &authCreds{Username: username, Password: password}) } } -// WithToken sets the key token to use -func WithToken(p string) source.Option { +// WithDialTimeout set the time out for dialing to etcd +func WithDialTimeout(timeout time.Duration) source.Option { return func(o *source.Options) { if o.Context == nil { o.Context = context.Background() } - o.Context = context.WithValue(o.Context, tokenKey{}, p) - } -} - -// WithConfig set consul-specific options -func WithConfig(c *api.Config) source.Option { - return func(o *source.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, configKey{}, c) + o.Context = context.WithValue(o.Context, dialTimeoutKey{}, timeout) } } diff --git a/config/source/etcd/util.go b/config/source/etcd/util.go new file mode 100644 index 00000000..31887fc9 --- /dev/null +++ b/config/source/etcd/util.go @@ -0,0 +1,89 @@ +package etcd + +import ( + "strings" + + "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/mvcc/mvccpb" + "github.com/micro/go-micro/config/encoder" +) + +func makeEvMap(e encoder.Encoder, data map[string]interface{}, kv []*clientv3.Event, stripPrefix string) map[string]interface{} { + if data == nil { + data = make(map[string]interface{}) + } + + for _, v := range kv { + switch mvccpb.Event_EventType(v.Type) { + case mvccpb.DELETE: + data = update(e, data, (*mvccpb.KeyValue)(v.Kv), "delete", stripPrefix) + default: + data = update(e, data, (*mvccpb.KeyValue)(v.Kv), "insert", stripPrefix) + } + } + + return data +} + +func makeMap(e encoder.Encoder, kv []*mvccpb.KeyValue, stripPrefix string) map[string]interface{} { + data := make(map[string]interface{}) + + for _, v := range kv { + data = update(e, data, v, "put", stripPrefix) + } + + return data +} + +func update(e encoder.Encoder, data map[string]interface{}, v *mvccpb.KeyValue, action, stripPrefix string) map[string]interface{} { + // remove prefix if non empty, and ensure leading / is removed as well + vkey := strings.TrimPrefix(strings.TrimPrefix(string(v.Key), stripPrefix), "/") + // split on prefix + haveSplit := strings.Contains(vkey, "/") + keys := strings.Split(vkey, "/") + + var vals interface{} + e.Decode(v.Value, &vals) + + if !haveSplit && len(keys) == 1 { + switch action { + case "delete": + data = make(map[string]interface{}) + default: + v, ok := vals.(map[string]interface{}) + if ok { + data = v + } + } + return data + } + + // set data for first iteration + kvals := data + // iterate the keys and make maps + for i, k := range keys { + kval, ok := kvals[k].(map[string]interface{}) + if !ok { + // create next map + kval = make(map[string]interface{}) + // set it + kvals[k] = kval + } + + // last key: write vals + if l := len(keys) - 1; i == l { + switch action { + case "delete": + delete(kvals, k) + default: + kvals[k] = vals + } + break + } + + // set kvals for next iterator + kvals = kval + } + + return data +} diff --git a/config/source/etcd/watcher.go b/config/source/etcd/watcher.go new file mode 100644 index 00000000..2f9b3189 --- /dev/null +++ b/config/source/etcd/watcher.go @@ -0,0 +1,113 @@ +package etcd + +import ( + "context" + "errors" + "sync" + "time" + + cetcd "github.com/coreos/etcd/clientv3" + "github.com/micro/go-micro/config/source" +) + +type watcher struct { + opts source.Options + name string + stripPrefix string + + sync.RWMutex + cs *source.ChangeSet + + ch chan *source.ChangeSet + exit chan bool +} + +func newWatcher(key, strip string, wc cetcd.Watcher, cs *source.ChangeSet, opts source.Options) (source.Watcher, error) { + w := &watcher{ + opts: opts, + name: "etcd", + stripPrefix: strip, + cs: cs, + ch: make(chan *source.ChangeSet), + exit: make(chan bool), + } + + ch := wc.Watch(context.Background(), key, cetcd.WithPrefix()) + + go w.run(wc, ch) + + return w, nil +} + +func (w *watcher) handle(evs []*cetcd.Event) { + w.RLock() + data := w.cs.Data + w.RUnlock() + + var vals map[string]interface{} + + // unpackage existing changeset + if err := w.opts.Encoder.Decode(data, &vals); err != nil { + return + } + + // update base changeset + d := makeEvMap(w.opts.Encoder, vals, evs, w.stripPrefix) + + // pack the changeset + b, err := w.opts.Encoder.Encode(d) + if err != nil { + return + } + + // create new changeset + cs := &source.ChangeSet{ + Timestamp: time.Now(), + Source: w.name, + Data: b, + Format: w.opts.Encoder.String(), + } + cs.Checksum = cs.Sum() + + // set base change set + w.Lock() + w.cs = cs + w.Unlock() + + // send update + w.ch <- cs +} + +func (w *watcher) run(wc cetcd.Watcher, ch cetcd.WatchChan) { + for { + select { + case rsp, ok := <-ch: + if !ok { + return + } + w.handle(rsp.Events) + case <-w.exit: + wc.Close() + return + } + } +} + +func (w *watcher) Next() (*source.ChangeSet, error) { + select { + case cs := <-w.ch: + return cs, nil + case <-w.exit: + return nil, errors.New("watcher stopped") + } +} + +func (w *watcher) Stop() error { + select { + case <-w.exit: + return nil + default: + close(w.exit) + } + return nil +} diff --git a/data/data.go b/data/data.go deleted file mode 100644 index eb6157d2..00000000 --- a/data/data.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package data is an interface for data access -package data diff --git a/data/store/consul/consul.go b/data/store/consul/consul.go deleted file mode 100644 index 174d043c..00000000 --- a/data/store/consul/consul.go +++ /dev/null @@ -1,96 +0,0 @@ -// Package consul is a consul implementation of kv -package consul - -import ( - "fmt" - "net" - - "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/config/options" - "github.com/micro/go-micro/data/store" -) - -type ckv struct { - options.Options - client *api.Client -} - -func (c *ckv) Read(key string) (*store.Record, error) { - keyval, _, err := c.client.KV().Get(key, nil) - if err != nil { - return nil, err - } - - if keyval == nil { - return nil, store.ErrNotFound - } - - return &store.Record{ - Key: keyval.Key, - Value: keyval.Value, - }, nil -} - -func (c *ckv) Delete(key string) error { - _, err := c.client.KV().Delete(key, nil) - return err -} - -func (c *ckv) Write(record *store.Record) error { - _, err := c.client.KV().Put(&api.KVPair{ - Key: record.Key, - Value: record.Value, - }, nil) - return err -} - -func (c *ckv) Dump() ([]*store.Record, error) { - keyval, _, err := c.client.KV().List("/", nil) - if err != nil { - return nil, err - } - if keyval == nil { - return nil, store.ErrNotFound - } - var vals []*store.Record - for _, keyv := range keyval { - vals = append(vals, &store.Record{ - Key: keyv.Key, - Value: keyv.Value, - }) - } - return vals, nil -} - -func (c *ckv) String() string { - return "consul" -} - -func NewStore(opts ...options.Option) store.Store { - options := options.NewOptions(opts...) - config := api.DefaultConfig() - - var nodes []string - - if n, ok := options.Values().Get("store.nodes"); ok { - nodes = n.([]string) - } - - // set host - if len(nodes) > 0 { - addr, port, err := net.SplitHostPort(nodes[0]) - if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { - port = "8500" - config.Address = fmt.Sprintf("%s:%s", nodes[0], port) - } else if err == nil { - config.Address = fmt.Sprintf("%s:%s", addr, port) - } - } - - client, _ := api.NewClient(config) - - return &ckv{ - Options: options, - client: client, - } -} diff --git a/debug/buffer/buffer.go b/debug/buffer/buffer.go new file mode 100644 index 00000000..007293aa --- /dev/null +++ b/debug/buffer/buffer.go @@ -0,0 +1,136 @@ +// Package buffer provides a simple ring buffer for storing local data +package buffer + +import ( + "sync" + "time" + + "github.com/google/uuid" +) + +type stream struct { + id string + entries chan *Entry + stop chan bool +} + +// Buffer is ring buffer +type Buffer struct { + size int + sync.RWMutex + vals []*Entry + streams map[string]stream +} + +// Entry is ring buffer data entry +type Entry struct { + Value interface{} + Timestamp time.Time +} + +// New returns a new buffer of the given size +func New(i int) *Buffer { + return &Buffer{ + size: i, + streams: make(map[string]stream), + } +} + +// Put adds a new value to ring buffer +func (b *Buffer) Put(v interface{}) { + b.Lock() + defer b.Unlock() + + // append to values + entry := &Entry{ + Value: v, + Timestamp: time.Now(), + } + b.vals = append(b.vals, entry) + + // trim if bigger than size required + if len(b.vals) > b.size { + b.vals = b.vals[1:] + } + + // TODO: this is fucking ugly + for _, stream := range b.streams { + select { + case <-stream.stop: + delete(b.streams, stream.id) + close(stream.entries) + case stream.entries <- entry: + } + } +} + +// Get returns the last n entries +func (b *Buffer) Get(n int) []*Entry { + b.RLock() + defer b.RUnlock() + + // reset any invalid values + if n > b.size || n < 0 { + n = b.size + } + + // create a delta + delta := b.size - n + + // if all the values are less than delta + if len(b.vals) < delta { + return b.vals + } + + // return the delta set + return b.vals[delta:] +} + +// Return the entries since a specific time +func (b *Buffer) Since(t time.Time) []*Entry { + b.RLock() + defer b.RUnlock() + + // return all the values + if t.IsZero() { + return b.vals + } + + // if its in the future return nothing + if time.Since(t).Seconds() < 0.0 { + return nil + } + + for i, v := range b.vals { + // find the starting point + d := v.Timestamp.Sub(t) + + // return the values + if d.Seconds() > 0.0 { + return b.vals[i:] + } + } + + return nil +} + +// Stream logs from the buffer +func (b *Buffer) Stream(stop chan bool) <-chan *Entry { + b.Lock() + defer b.Unlock() + + entries := make(chan *Entry, 128) + id := uuid.New().String() + b.streams[id] = stream{ + id: id, + entries: entries, + stop: stop, + } + + return entries +} + +// Size returns the size of the ring buffer +func (b *Buffer) Size() int { + return b.size +} diff --git a/debug/buffer/buffer_test.go b/debug/buffer/buffer_test.go new file mode 100644 index 00000000..3a107923 --- /dev/null +++ b/debug/buffer/buffer_test.go @@ -0,0 +1,79 @@ +package buffer + +import ( + "testing" + "time" +) + +func TestBuffer(t *testing.T) { + b := New(10) + + // test one value + b.Put("foo") + v := b.Get(1) + + if val := v[0].Value.(string); val != "foo" { + t.Fatalf("expected foo got %v", val) + } + + b = New(10) + + // test 10 values + for i := 0; i < 10; i++ { + b.Put(i) + } + + d := time.Now() + v = b.Get(10) + + for i := 0; i < 10; i++ { + val := v[i].Value.(int) + + if val != i { + t.Fatalf("expected %d got %d", i, val) + } + } + + // test more values + + for i := 0; i < 10; i++ { + v := i * 2 + b.Put(v) + } + + v = b.Get(10) + + for i := 0; i < 10; i++ { + val := v[i].Value.(int) + expect := i * 2 + if val != expect { + t.Fatalf("expected %d got %d", expect, val) + } + } + + // sleep 100 ms + time.Sleep(time.Millisecond * 100) + + // assume we'll get everything + v = b.Since(d) + + if len(v) != 10 { + t.Fatalf("expected 10 entries but got %d", len(v)) + } + + // write 1 more entry + d = time.Now() + b.Put(100) + + // sleep 100 ms + time.Sleep(time.Millisecond * 100) + + v = b.Since(d) + if len(v) != 1 { + t.Fatalf("expected 1 entries but got %d", len(v)) + } + + if v[0].Value.(int) != 100 { + t.Fatalf("expected value 100 got %v", v[0]) + } +} diff --git a/debug/debug.go b/debug/debug.go new file mode 100644 index 00000000..a34d7f28 --- /dev/null +++ b/debug/debug.go @@ -0,0 +1,7 @@ +// Package debug provides micro debug packages +package debug + +var ( + // DefaultName is the name of debug service + DefaultName = "go.micro.debug" +) diff --git a/debug/handler/debug.go b/debug/handler/debug.go deleted file mode 100644 index 973a6fc4..00000000 --- a/debug/handler/debug.go +++ /dev/null @@ -1,41 +0,0 @@ -package handler - -import ( - "context" - "runtime" - "time" - - proto "github.com/micro/go-micro/debug/proto" -) - -type Debug struct { - proto.DebugHandler - started int64 -} - -var ( - DefaultHandler = newDebug() -) - -func newDebug() *Debug { - return &Debug{ - started: time.Now().Unix(), - } -} - -func (d *Debug) Health(ctx context.Context, req *proto.HealthRequest, rsp *proto.HealthResponse) error { - rsp.Status = "ok" - return nil -} - -func (d *Debug) Stats(ctx context.Context, req *proto.StatsRequest, rsp *proto.StatsResponse) error { - var mstat runtime.MemStats - runtime.ReadMemStats(&mstat) - - rsp.Started = uint64(d.started) - rsp.Uptime = uint64(time.Now().Unix() - d.started) - rsp.Memory = mstat.Alloc - rsp.Gc = mstat.PauseTotalNs - rsp.Threads = uint64(runtime.NumGoroutine()) - return nil -} diff --git a/debug/log/default.go b/debug/log/default.go new file mode 100644 index 00000000..6f7bddab --- /dev/null +++ b/debug/log/default.go @@ -0,0 +1,114 @@ +package log + +import ( + "fmt" + golog "log" + + "github.com/micro/go-micro/debug/buffer" +) + +var ( + // DefaultSize of the logger buffer + DefaultSize = 1000 +) + +// defaultLog is default micro log +type defaultLog struct { + *buffer.Buffer +} + +// NewLog returns default Logger with +func NewLog(opts ...Option) Log { + // get default options + options := DefaultOptions() + + // apply requested options + for _, o := range opts { + o(&options) + } + + return &defaultLog{ + Buffer: buffer.New(options.Size), + } +} + +// Write writes logs into logger +func (l *defaultLog) Write(r Record) { + golog.Print(r.Value) + l.Buffer.Put(fmt.Sprint(r.Value)) +} + +// Read reads logs and returns them +func (l *defaultLog) Read(opts ...ReadOption) []Record { + options := ReadOptions{} + // initialize the read options + for _, o := range opts { + o(&options) + } + + var entries []*buffer.Entry + // if Since options ha sbeen specified we honor it + if !options.Since.IsZero() { + entries = l.Buffer.Since(options.Since) + } + + // only if we specified valid count constraint + // do we end up doing some serious if-else kung-fu + // if since constraint has been provided + // we return *count* number of logs since the given timestamp; + // otherwise we return last count number of logs + if options.Count > 0 { + switch len(entries) > 0 { + case true: + // if we request fewer logs than what since constraint gives us + if options.Count < len(entries) { + entries = entries[0:options.Count] + } + default: + entries = l.Buffer.Get(options.Count) + } + } + + records := make([]Record, 0, len(entries)) + for _, entry := range entries { + record := Record{ + Timestamp: entry.Timestamp, + Value: entry.Value, + } + records = append(records, record) + } + + return records +} + +// Stream returns channel for reading log records +func (l *defaultLog) Stream(stop chan bool) <-chan Record { + // get stream channel from ring buffer + stream := l.Buffer.Stream(stop) + // make a buffered channel + records := make(chan Record, 128) + // get last 10 records + last10 := l.Buffer.Get(10) + + // stream the log records + go func() { + // first send last 10 records + for _, entry := range last10 { + records <- Record{ + Timestamp: entry.Timestamp, + Value: entry.Value, + Metadata: make(map[string]string), + } + } + // now stream continuously + for entry := range stream { + records <- Record{ + Timestamp: entry.Timestamp, + Value: entry.Value, + Metadata: make(map[string]string), + } + } + }() + + return records +} diff --git a/debug/log/default_test.go b/debug/log/default_test.go new file mode 100644 index 00000000..e0f08a7c --- /dev/null +++ b/debug/log/default_test.go @@ -0,0 +1,32 @@ +package log + +import ( + "reflect" + "testing" +) + +func TestLogger(t *testing.T) { + // set size to some value + size := 100 + // override the global logger + DefaultLog = NewLog(Size(size)) + // make sure we have the right size of the logger ring buffer + if DefaultLog.(*defaultLog).Size() != size { + t.Errorf("expected buffer size: %d, got: %d", size, DefaultLog.(*defaultLog).Size()) + } + + // Log some cruft + Info("foobar") + // increase the log level + DefaultLevel = LevelDebug + Debugf("foo %s", "bar") + + // Check if the logs are stored in the logger ring buffer + expected := []string{"foobar", "foo bar"} + entries := DefaultLog.Read(Count(len(expected))) + for i, entry := range entries { + if !reflect.DeepEqual(entry.Value, expected[i]) { + t.Errorf("expected %s, got %s", expected[i], entry.Value) + } + } +} diff --git a/debug/log/log.go b/debug/log/log.go new file mode 100644 index 00000000..42a8f558 --- /dev/null +++ b/debug/log/log.go @@ -0,0 +1,179 @@ +// Package log provides debug logging +package log + +import ( + "fmt" + "os" + "time" +) + +var ( + // DefaultLog logger + DefaultLog = NewLog() + // DefaultLevel is default log level + DefaultLevel = LevelInfo + // prefix for all messages + prefix string +) + +// Log is event log +type Log interface { + // Read reads log entries from the logger + Read(...ReadOption) []Record + // Write writes records to log + Write(Record) + // Stream log records + Stream(chan bool) <-chan Record +} + +// Record is log record entry +type Record struct { + // Timestamp of logged event + Timestamp time.Time + // Value contains log entry + Value interface{} + // Metadata to enrich log record + Metadata map[string]string +} + +// level is a log level +type Level int + +const ( + LevelFatal Level = iota + LevelError + LevelInfo + LevelWarn + LevelDebug + LevelTrace +) + +func init() { + switch os.Getenv("MICRO_LOG_LEVEL") { + case "trace": + DefaultLevel = LevelTrace + case "debug": + DefaultLevel = LevelDebug + case "warn": + DefaultLevel = LevelWarn + case "info": + DefaultLevel = LevelInfo + case "error": + DefaultLevel = LevelError + case "fatal": + DefaultLevel = LevelFatal + } +} + +func log(v ...interface{}) { + if len(prefix) > 0 { + DefaultLog.Write(Record{Value: fmt.Sprint(append([]interface{}{prefix, " "}, v...)...)}) + return + } + DefaultLog.Write(Record{Value: fmt.Sprint(v...)}) +} + +func logf(format string, v ...interface{}) { + if len(prefix) > 0 { + format = prefix + " " + format + } + DefaultLog.Write(Record{Value: fmt.Sprintf(format, v...)}) +} + +// WithLevel logs with the level specified +func WithLevel(l Level, v ...interface{}) { + if l > DefaultLevel { + return + } + log(v...) +} + +// WithLevel logs with the level specified +func WithLevelf(l Level, format string, v ...interface{}) { + if l > DefaultLevel { + return + } + logf(format, v...) +} + +// Trace provides trace level logging +func Trace(v ...interface{}) { + WithLevel(LevelTrace, v...) +} + +// Tracef provides trace level logging +func Tracef(format string, v ...interface{}) { + WithLevelf(LevelTrace, format, v...) +} + +// Debug provides debug level logging +func Debug(v ...interface{}) { + WithLevel(LevelDebug, v...) +} + +// Debugf provides debug level logging +func Debugf(format string, v ...interface{}) { + WithLevelf(LevelDebug, format, v...) +} + +// Warn provides warn level logging +func Warn(v ...interface{}) { + WithLevel(LevelWarn, v...) +} + +// Warnf provides warn level logging +func Warnf(format string, v ...interface{}) { + WithLevelf(LevelWarn, format, v...) +} + +// Info provides info level logging +func Info(v ...interface{}) { + WithLevel(LevelInfo, v...) +} + +// Infof provides info level logging +func Infof(format string, v ...interface{}) { + WithLevelf(LevelInfo, format, v...) +} + +// Error provides warn level logging +func Error(v ...interface{}) { + WithLevel(LevelError, v...) +} + +// Errorf provides warn level logging +func Errorf(format string, v ...interface{}) { + WithLevelf(LevelError, format, v...) +} + +// Fatal logs with Log and then exits with os.Exit(1) +func Fatal(v ...interface{}) { + WithLevel(LevelFatal, v...) + os.Exit(1) +} + +// Fatalf logs with Logf and then exits with os.Exit(1) +func Fatalf(format string, v ...interface{}) { + WithLevelf(LevelFatal, format, v...) + os.Exit(1) +} + +// SetLevel sets the log level +func SetLevel(l Level) { + DefaultLevel = l +} + +// GetLevel returns the current level +func GetLevel() Level { + return DefaultLevel +} + +// Set a prefix for the logger +func SetPrefix(p string) { + prefix = p +} + +// Set service name +func Name(name string) { + prefix = fmt.Sprintf("[%s]", name) +} diff --git a/debug/log/options.go b/debug/log/options.go new file mode 100644 index 00000000..03dece38 --- /dev/null +++ b/debug/log/options.go @@ -0,0 +1,60 @@ +package log + +import "time" + +// Option used by the logger +type Option func(*Options) + +// Options are logger options +type Options struct { + // Size is the size of ring buffer + Size int +} + +// Size sets the size of the ring buffer +func Size(s int) Option { + return func(o *Options) { + o.Size = s + } +} + +// DefaultOptions returns default options +func DefaultOptions() Options { + return Options{ + Size: DefaultSize, + } +} + +// ReadOptions for querying the logs +type ReadOptions struct { + // Since what time in past to return the logs + Since time.Time + // Count specifies number of logs to return + Count int + // Stream requests continuous log stream + Stream bool +} + +// ReadOption used for reading the logs +type ReadOption func(*ReadOptions) + +// Since sets the time since which to return the log records +func Since(s time.Time) ReadOption { + return func(o *ReadOptions) { + o.Since = s + } +} + +// Count sets the number of log records to return +func Count(c int) ReadOption { + return func(o *ReadOptions) { + o.Count = c + } +} + +// Stream requests continuous log stream +func Stream(s bool) ReadOption { + return func(o *ReadOptions) { + o.Stream = s + } +} diff --git a/debug/profile/http/http.go b/debug/profile/http/http.go new file mode 100644 index 00000000..66cbf49c --- /dev/null +++ b/debug/profile/http/http.go @@ -0,0 +1,78 @@ +// Package http enables the http profiler +package http + +import ( + "context" + "net/http" + "net/http/pprof" + "sync" + + "github.com/micro/go-micro/debug/profile" +) + +type httpProfile struct { + sync.Mutex + running bool + server *http.Server +} + +var ( + DefaultAddress = ":6060" +) + +// Start the profiler +func (h *httpProfile) Start() error { + h.Lock() + defer h.Unlock() + + if h.running { + return nil + } + + go func() { + if err := h.server.ListenAndServe(); err != nil { + h.Lock() + h.running = false + h.Unlock() + } + }() + + h.running = true + + return nil +} + +// Stop the profiler +func (h *httpProfile) Stop() error { + h.Lock() + defer h.Unlock() + + if !h.running { + return nil + } + + h.running = false + + return h.server.Shutdown(context.TODO()) +} + +func (h *httpProfile) String() string { + return "http" +} + +func NewProfile(opts ...profile.Option) profile.Profile { + mux := http.NewServeMux() + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + return &httpProfile{ + server: &http.Server{ + Addr: DefaultAddress, + Handler: mux, + }, + } +} diff --git a/debug/profile/pprof/pprof.go b/debug/profile/pprof/pprof.go new file mode 100644 index 00000000..5dfd49ba --- /dev/null +++ b/debug/profile/pprof/pprof.go @@ -0,0 +1,122 @@ +// Package pprof provides a pprof profiler +package pprof + +import ( + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "sync" + "time" + + "github.com/micro/go-micro/debug/profile" +) + +type profiler struct { + opts profile.Options + + sync.Mutex + running bool + exit chan bool + + // where the cpu profile is written + cpuFile *os.File + // where the mem profile is written + memFile *os.File +} + +func (p *profiler) writeHeap(f *os.File) { + defer f.Close() + + t := time.NewTicker(time.Second * 30) + defer t.Stop() + + for { + select { + case <-t.C: + runtime.GC() + pprof.WriteHeapProfile(f) + case <-p.exit: + return + } + } +} + +func (p *profiler) Start() error { + p.Lock() + defer p.Unlock() + + if p.running { + return nil + } + + // create exit channel + p.exit = make(chan bool) + + cpuFile := filepath.Join("/tmp", "cpu.pprof") + memFile := filepath.Join("/tmp", "mem.pprof") + + if len(p.opts.Name) > 0 { + cpuFile = filepath.Join("/tmp", p.opts.Name+".cpu.pprof") + memFile = filepath.Join("/tmp", p.opts.Name+".mem.pprof") + } + + f1, err := os.Create(cpuFile) + if err != nil { + return err + } + + f2, err := os.Create(memFile) + if err != nil { + return err + } + + // start cpu profiling + if err := pprof.StartCPUProfile(f1); err != nil { + return err + } + + // write the heap periodically + go p.writeHeap(f2) + + // set cpu file + p.cpuFile = f1 + // set mem file + p.memFile = f2 + + p.running = true + + return nil +} + +func (p *profiler) Stop() error { + p.Lock() + defer p.Unlock() + + select { + case <-p.exit: + return nil + default: + close(p.exit) + pprof.StopCPUProfile() + p.cpuFile.Close() + p.running = false + p.cpuFile = nil + p.memFile = nil + return nil + } +} + +func (p *profiler) String() string { + return "pprof" +} + +func NewProfile(opts ...profile.Option) profile.Profile { + var options profile.Options + for _, o := range opts { + o(&options) + } + p := new(profiler) + p.opts = options + return p +} diff --git a/debug/profile/profile.go b/debug/profile/profile.go new file mode 100644 index 00000000..3cc075f4 --- /dev/null +++ b/debug/profile/profile.go @@ -0,0 +1,25 @@ +// Package profile is for profilers +package profile + +type Profile interface { + // Start the profiler + Start() error + // Stop the profiler + Stop() error + // Name of the profiler + String() string +} + +type Options struct { + // Name to use for the profile + Name string +} + +type Option func(o *Options) + +// Name of the profile +func Name(n string) Option { + return func(o *Options) { + o.Name = n + } +} diff --git a/debug/proto/debug.pb.go b/debug/proto/debug.pb.go deleted file mode 100644 index 5ba4f8c9..00000000 --- a/debug/proto/debug.pb.go +++ /dev/null @@ -1,336 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: micro/go-micro/debug/proto/debug.proto - -package debug - -import ( - context "context" - fmt "fmt" - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" - math "math" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// 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 - -type HealthRequest struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *HealthRequest) Reset() { *m = HealthRequest{} } -func (m *HealthRequest) String() string { return proto.CompactTextString(m) } -func (*HealthRequest) ProtoMessage() {} -func (*HealthRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_f25415e61bccfa1f, []int{0} -} - -func (m *HealthRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_HealthRequest.Unmarshal(m, b) -} -func (m *HealthRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_HealthRequest.Marshal(b, m, deterministic) -} -func (m *HealthRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_HealthRequest.Merge(m, src) -} -func (m *HealthRequest) XXX_Size() int { - return xxx_messageInfo_HealthRequest.Size(m) -} -func (m *HealthRequest) XXX_DiscardUnknown() { - xxx_messageInfo_HealthRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_HealthRequest proto.InternalMessageInfo - -type HealthResponse struct { - // default: ok - Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *HealthResponse) Reset() { *m = HealthResponse{} } -func (m *HealthResponse) String() string { return proto.CompactTextString(m) } -func (*HealthResponse) ProtoMessage() {} -func (*HealthResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_f25415e61bccfa1f, []int{1} -} - -func (m *HealthResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_HealthResponse.Unmarshal(m, b) -} -func (m *HealthResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_HealthResponse.Marshal(b, m, deterministic) -} -func (m *HealthResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_HealthResponse.Merge(m, src) -} -func (m *HealthResponse) XXX_Size() int { - return xxx_messageInfo_HealthResponse.Size(m) -} -func (m *HealthResponse) XXX_DiscardUnknown() { - xxx_messageInfo_HealthResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_HealthResponse proto.InternalMessageInfo - -func (m *HealthResponse) GetStatus() string { - if m != nil { - return m.Status - } - return "" -} - -type StatsRequest struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *StatsRequest) Reset() { *m = StatsRequest{} } -func (m *StatsRequest) String() string { return proto.CompactTextString(m) } -func (*StatsRequest) ProtoMessage() {} -func (*StatsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_f25415e61bccfa1f, []int{2} -} - -func (m *StatsRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_StatsRequest.Unmarshal(m, b) -} -func (m *StatsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_StatsRequest.Marshal(b, m, deterministic) -} -func (m *StatsRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_StatsRequest.Merge(m, src) -} -func (m *StatsRequest) XXX_Size() int { - return xxx_messageInfo_StatsRequest.Size(m) -} -func (m *StatsRequest) XXX_DiscardUnknown() { - xxx_messageInfo_StatsRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_StatsRequest proto.InternalMessageInfo - -type StatsResponse struct { - // unix timestamp - Started uint64 `protobuf:"varint,1,opt,name=started,proto3" json:"started,omitempty"` - // in seconds - Uptime uint64 `protobuf:"varint,2,opt,name=uptime,proto3" json:"uptime,omitempty"` - // in bytes - Memory uint64 `protobuf:"varint,3,opt,name=memory,proto3" json:"memory,omitempty"` - // num threads - Threads uint64 `protobuf:"varint,4,opt,name=threads,proto3" json:"threads,omitempty"` - // total gc in nanoseconds - Gc uint64 `protobuf:"varint,5,opt,name=gc,proto3" json:"gc,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *StatsResponse) Reset() { *m = StatsResponse{} } -func (m *StatsResponse) String() string { return proto.CompactTextString(m) } -func (*StatsResponse) ProtoMessage() {} -func (*StatsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_f25415e61bccfa1f, []int{3} -} - -func (m *StatsResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_StatsResponse.Unmarshal(m, b) -} -func (m *StatsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_StatsResponse.Marshal(b, m, deterministic) -} -func (m *StatsResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_StatsResponse.Merge(m, src) -} -func (m *StatsResponse) XXX_Size() int { - return xxx_messageInfo_StatsResponse.Size(m) -} -func (m *StatsResponse) XXX_DiscardUnknown() { - xxx_messageInfo_StatsResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_StatsResponse proto.InternalMessageInfo - -func (m *StatsResponse) GetStarted() uint64 { - if m != nil { - return m.Started - } - return 0 -} - -func (m *StatsResponse) GetUptime() uint64 { - if m != nil { - return m.Uptime - } - return 0 -} - -func (m *StatsResponse) GetMemory() uint64 { - if m != nil { - return m.Memory - } - return 0 -} - -func (m *StatsResponse) GetThreads() uint64 { - if m != nil { - return m.Threads - } - return 0 -} - -func (m *StatsResponse) GetGc() uint64 { - if m != nil { - return m.Gc - } - return 0 -} - -func init() { - proto.RegisterType((*HealthRequest)(nil), "HealthRequest") - proto.RegisterType((*HealthResponse)(nil), "HealthResponse") - proto.RegisterType((*StatsRequest)(nil), "StatsRequest") - proto.RegisterType((*StatsResponse)(nil), "StatsResponse") -} - -func init() { - proto.RegisterFile("micro/go-micro/debug/proto/debug.proto", fileDescriptor_f25415e61bccfa1f) -} - -var fileDescriptor_f25415e61bccfa1f = []byte{ - // 230 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0x41, 0x4b, 0xc4, 0x30, - 0x10, 0x85, 0xb7, 0x75, 0x5b, 0x71, 0xb0, 0x59, 0xc8, 0x41, 0xc2, 0x9e, 0x24, 0x07, 0x29, 0x88, - 0x59, 0xd0, 0xbf, 0xe0, 0xc1, 0x73, 0xbd, 0x0b, 0xd9, 0x76, 0xe8, 0x16, 0xac, 0xa9, 0x99, 0xe9, - 0xc1, 0xb3, 0x7f, 0x5c, 0x9a, 0xa4, 0x60, 0x6f, 0xef, 0xbd, 0xf0, 0x1e, 0xf9, 0x06, 0x1e, 0xc6, - 0xa1, 0xf5, 0xee, 0xd4, 0xbb, 0xa7, 0x28, 0x3a, 0x3c, 0xcf, 0xfd, 0x69, 0xf2, 0x8e, 0x93, 0x36, - 0x41, 0xeb, 0x03, 0x54, 0x6f, 0x68, 0x3f, 0xf9, 0xd2, 0xe0, 0xf7, 0x8c, 0xc4, 0xba, 0x06, 0xb1, - 0x06, 0x34, 0xb9, 0x2f, 0x42, 0x79, 0x07, 0x25, 0xb1, 0xe5, 0x99, 0x54, 0x76, 0x9f, 0xd5, 0x37, - 0x4d, 0x72, 0x5a, 0xc0, 0xed, 0x3b, 0x5b, 0xa6, 0xb5, 0xf9, 0x9b, 0x41, 0x95, 0x82, 0xd4, 0x54, - 0x70, 0x4d, 0x6c, 0x3d, 0x63, 0x17, 0xaa, 0xfb, 0x66, 0xb5, 0xcb, 0xe6, 0x3c, 0xf1, 0x30, 0xa2, - 0xca, 0xc3, 0x43, 0x72, 0x4b, 0x3e, 0xe2, 0xe8, 0xfc, 0x8f, 0xba, 0x8a, 0x79, 0x74, 0xcb, 0x12, - 0x5f, 0x3c, 0xda, 0x8e, 0xd4, 0x3e, 0x2e, 0x25, 0x2b, 0x05, 0xe4, 0x7d, 0xab, 0x8a, 0x10, 0xe6, - 0x7d, 0xfb, 0xfc, 0x01, 0xc5, 0xeb, 0xc2, 0x27, 0x1f, 0xa1, 0x8c, 0x20, 0x52, 0x98, 0x0d, 0xe2, - 0xf1, 0x60, 0xb6, 0x84, 0x7a, 0x27, 0x6b, 0x28, 0xc2, 0xd7, 0x65, 0x65, 0xfe, 0x33, 0x1d, 0x85, - 0xd9, 0x10, 0xe9, 0xdd, 0xb9, 0x0c, 0x77, 0x7b, 0xf9, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xfe, 0xb9, - 0x5f, 0xf7, 0x61, 0x01, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// DebugClient is the client API for Debug service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type DebugClient interface { - Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) - Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsResponse, error) -} - -type debugClient struct { - cc *grpc.ClientConn -} - -func NewDebugClient(cc *grpc.ClientConn) DebugClient { - return &debugClient{cc} -} - -func (c *debugClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) { - out := new(HealthResponse) - err := c.cc.Invoke(ctx, "/Debug/Health", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *debugClient) Stats(ctx context.Context, in *StatsRequest, opts ...grpc.CallOption) (*StatsResponse, error) { - out := new(StatsResponse) - err := c.cc.Invoke(ctx, "/Debug/Stats", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// DebugServer is the server API for Debug service. -type DebugServer interface { - Health(context.Context, *HealthRequest) (*HealthResponse, error) - Stats(context.Context, *StatsRequest) (*StatsResponse, error) -} - -func RegisterDebugServer(s *grpc.Server, srv DebugServer) { - s.RegisterService(&_Debug_serviceDesc, srv) -} - -func _Debug_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HealthRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DebugServer).Health(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/Debug/Health", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DebugServer).Health(ctx, req.(*HealthRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Debug_Stats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(StatsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DebugServer).Stats(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/Debug/Stats", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DebugServer).Stats(ctx, req.(*StatsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _Debug_serviceDesc = grpc.ServiceDesc{ - ServiceName: "Debug", - HandlerType: (*DebugServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Health", - Handler: _Debug_Health_Handler, - }, - { - MethodName: "Stats", - Handler: _Debug_Stats_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "micro/go-micro/debug/proto/debug.proto", -} diff --git a/debug/proto/debug.proto b/debug/proto/debug.proto deleted file mode 100644 index b642cd41..00000000 --- a/debug/proto/debug.proto +++ /dev/null @@ -1,30 +0,0 @@ -syntax = "proto3"; - -service Debug { - rpc Health(HealthRequest) returns (HealthResponse) {} - rpc Stats(StatsRequest) returns (StatsResponse) {} -} - -message HealthRequest { -} - -message HealthResponse { - // default: ok - string status = 1; -} - -message StatsRequest { -} - -message StatsResponse { - // unix timestamp - uint64 started = 1; - // in seconds - uint64 uptime = 2; - // in bytes - uint64 memory = 3; - // num threads - uint64 threads = 4; - // total gc in nanoseconds - uint64 gc = 5; -} diff --git a/debug/service/handler/debug.go b/debug/service/handler/debug.go new file mode 100644 index 00000000..ee24b27a --- /dev/null +++ b/debug/service/handler/debug.go @@ -0,0 +1,116 @@ +// Pacjage handler implements service debug handler +package handler + +import ( + "context" + "runtime" + "time" + + "github.com/micro/go-micro/debug/log" + proto "github.com/micro/go-micro/debug/service/proto" + "github.com/micro/go-micro/server" +) + +var ( + // DefaultHandler is default debug handler + DefaultHandler = newDebug() +) + +type Debug struct { + started int64 + proto.DebugHandler + log log.Log +} + +func newDebug() *Debug { + return &Debug{ + started: time.Now().Unix(), + log: log.DefaultLog, + } +} + +func (d *Debug) Health(ctx context.Context, req *proto.HealthRequest, rsp *proto.HealthResponse) error { + rsp.Status = "ok" + return nil +} + +func (d *Debug) Stats(ctx context.Context, req *proto.StatsRequest, rsp *proto.StatsResponse) error { + var mstat runtime.MemStats + runtime.ReadMemStats(&mstat) + + rsp.Timestamp = uint64(time.Now().Unix()) + rsp.Started = uint64(d.started) + rsp.Uptime = uint64(time.Now().Unix() - d.started) + rsp.Memory = mstat.Alloc + rsp.Gc = mstat.PauseTotalNs + rsp.Threads = uint64(runtime.NumGoroutine()) + return nil +} + +func (d *Debug) Log(ctx context.Context, stream server.Stream) error { + req := new(proto.LogRequest) + if err := stream.Recv(req); err != nil { + return err + } + + var options []log.ReadOption + + since := time.Unix(req.Since, 0) + if !since.IsZero() { + options = append(options, log.Since(since)) + } + + count := int(req.Count) + if count > 0 { + options = append(options, log.Count(count)) + } + + if req.Stream { + stop := make(chan bool) + defer close(stop) + + // TODO: we need to figure out how to close ithe log stream + // It seems like when a client disconnects, + // the connection stays open until some timeout expires + // or something like that; that means the map of streams + // might end up leaking memory if not cleaned up properly + records := d.log.Stream(stop) + for record := range records { + if err := d.sendRecord(record, stream); err != nil { + return err + } + } + // done streaming, return + return nil + } + + // get the log records + records := d.log.Read(options...) + // send all the logs downstream + for _, record := range records { + if err := d.sendRecord(record, stream); err != nil { + return err + } + } + + return nil +} + +func (d *Debug) sendRecord(record log.Record, stream server.Stream) error { + metadata := make(map[string]string) + for k, v := range record.Metadata { + metadata[k] = v + } + + pbRecord := &proto.Record{ + Timestamp: record.Timestamp.Unix(), + Value: record.Value.(string), + Metadata: metadata, + } + + if err := stream.Send(pbRecord); err != nil { + return err + } + + return nil +} diff --git a/debug/service/proto/debug.pb.go b/debug/service/proto/debug.pb.go new file mode 100644 index 00000000..f76bbba3 --- /dev/null +++ b/debug/service/proto/debug.pb.go @@ -0,0 +1,398 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: micro/go-micro/debug/service/proto/debug.proto + +package debug + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// 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 + +type HealthRequest struct { + // optional service name + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HealthRequest) Reset() { *m = HealthRequest{} } +func (m *HealthRequest) String() string { return proto.CompactTextString(m) } +func (*HealthRequest) ProtoMessage() {} +func (*HealthRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_dea322649cde1ef2, []int{0} +} + +func (m *HealthRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HealthRequest.Unmarshal(m, b) +} +func (m *HealthRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HealthRequest.Marshal(b, m, deterministic) +} +func (m *HealthRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_HealthRequest.Merge(m, src) +} +func (m *HealthRequest) XXX_Size() int { + return xxx_messageInfo_HealthRequest.Size(m) +} +func (m *HealthRequest) XXX_DiscardUnknown() { + xxx_messageInfo_HealthRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_HealthRequest proto.InternalMessageInfo + +func (m *HealthRequest) GetService() string { + if m != nil { + return m.Service + } + return "" +} + +type HealthResponse struct { + // default: ok + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *HealthResponse) Reset() { *m = HealthResponse{} } +func (m *HealthResponse) String() string { return proto.CompactTextString(m) } +func (*HealthResponse) ProtoMessage() {} +func (*HealthResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_dea322649cde1ef2, []int{1} +} + +func (m *HealthResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_HealthResponse.Unmarshal(m, b) +} +func (m *HealthResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_HealthResponse.Marshal(b, m, deterministic) +} +func (m *HealthResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_HealthResponse.Merge(m, src) +} +func (m *HealthResponse) XXX_Size() int { + return xxx_messageInfo_HealthResponse.Size(m) +} +func (m *HealthResponse) XXX_DiscardUnknown() { + xxx_messageInfo_HealthResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_HealthResponse proto.InternalMessageInfo + +func (m *HealthResponse) GetStatus() string { + if m != nil { + return m.Status + } + return "" +} + +type StatsRequest struct { + // optional service name + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *StatsRequest) Reset() { *m = StatsRequest{} } +func (m *StatsRequest) String() string { return proto.CompactTextString(m) } +func (*StatsRequest) ProtoMessage() {} +func (*StatsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_dea322649cde1ef2, []int{2} +} + +func (m *StatsRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_StatsRequest.Unmarshal(m, b) +} +func (m *StatsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_StatsRequest.Marshal(b, m, deterministic) +} +func (m *StatsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_StatsRequest.Merge(m, src) +} +func (m *StatsRequest) XXX_Size() int { + return xxx_messageInfo_StatsRequest.Size(m) +} +func (m *StatsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_StatsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_StatsRequest proto.InternalMessageInfo + +func (m *StatsRequest) GetService() string { + if m != nil { + return m.Service + } + return "" +} + +type StatsResponse struct { + // timestamp of recording + Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // unix timestamp + Started uint64 `protobuf:"varint,2,opt,name=started,proto3" json:"started,omitempty"` + // in seconds + Uptime uint64 `protobuf:"varint,3,opt,name=uptime,proto3" json:"uptime,omitempty"` + // in bytes + Memory uint64 `protobuf:"varint,4,opt,name=memory,proto3" json:"memory,omitempty"` + // num threads + Threads uint64 `protobuf:"varint,5,opt,name=threads,proto3" json:"threads,omitempty"` + // total gc in nanoseconds + Gc uint64 `protobuf:"varint,6,opt,name=gc,proto3" json:"gc,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *StatsResponse) Reset() { *m = StatsResponse{} } +func (m *StatsResponse) String() string { return proto.CompactTextString(m) } +func (*StatsResponse) ProtoMessage() {} +func (*StatsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_dea322649cde1ef2, []int{3} +} + +func (m *StatsResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_StatsResponse.Unmarshal(m, b) +} +func (m *StatsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_StatsResponse.Marshal(b, m, deterministic) +} +func (m *StatsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_StatsResponse.Merge(m, src) +} +func (m *StatsResponse) XXX_Size() int { + return xxx_messageInfo_StatsResponse.Size(m) +} +func (m *StatsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_StatsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_StatsResponse proto.InternalMessageInfo + +func (m *StatsResponse) GetTimestamp() uint64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +func (m *StatsResponse) GetStarted() uint64 { + if m != nil { + return m.Started + } + return 0 +} + +func (m *StatsResponse) GetUptime() uint64 { + if m != nil { + return m.Uptime + } + return 0 +} + +func (m *StatsResponse) GetMemory() uint64 { + if m != nil { + return m.Memory + } + return 0 +} + +func (m *StatsResponse) GetThreads() uint64 { + if m != nil { + return m.Threads + } + return 0 +} + +func (m *StatsResponse) GetGc() uint64 { + if m != nil { + return m.Gc + } + return 0 +} + +// LogRequest requests service logs +type LogRequest struct { + // service to request logs for + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + // stream records continuously + Stream bool `protobuf:"varint,2,opt,name=stream,proto3" json:"stream,omitempty"` + // count of records to request + Count int64 `protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"` + // relative time in seconds + // before the current time + // from which to show logs + Since int64 `protobuf:"varint,4,opt,name=since,proto3" json:"since,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LogRequest) Reset() { *m = LogRequest{} } +func (m *LogRequest) String() string { return proto.CompactTextString(m) } +func (*LogRequest) ProtoMessage() {} +func (*LogRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_dea322649cde1ef2, []int{4} +} + +func (m *LogRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LogRequest.Unmarshal(m, b) +} +func (m *LogRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LogRequest.Marshal(b, m, deterministic) +} +func (m *LogRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_LogRequest.Merge(m, src) +} +func (m *LogRequest) XXX_Size() int { + return xxx_messageInfo_LogRequest.Size(m) +} +func (m *LogRequest) XXX_DiscardUnknown() { + xxx_messageInfo_LogRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_LogRequest proto.InternalMessageInfo + +func (m *LogRequest) GetService() string { + if m != nil { + return m.Service + } + return "" +} + +func (m *LogRequest) GetStream() bool { + if m != nil { + return m.Stream + } + return false +} + +func (m *LogRequest) GetCount() int64 { + if m != nil { + return m.Count + } + return 0 +} + +func (m *LogRequest) GetSince() int64 { + if m != nil { + return m.Since + } + return 0 +} + +// Record is service log record +type Record struct { + // timestamp of log record + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // record value + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + // record metadata + Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Record) Reset() { *m = Record{} } +func (m *Record) String() string { return proto.CompactTextString(m) } +func (*Record) ProtoMessage() {} +func (*Record) Descriptor() ([]byte, []int) { + return fileDescriptor_dea322649cde1ef2, []int{5} +} + +func (m *Record) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Record.Unmarshal(m, b) +} +func (m *Record) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Record.Marshal(b, m, deterministic) +} +func (m *Record) XXX_Merge(src proto.Message) { + xxx_messageInfo_Record.Merge(m, src) +} +func (m *Record) XXX_Size() int { + return xxx_messageInfo_Record.Size(m) +} +func (m *Record) XXX_DiscardUnknown() { + xxx_messageInfo_Record.DiscardUnknown(m) +} + +var xxx_messageInfo_Record proto.InternalMessageInfo + +func (m *Record) GetTimestamp() int64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +func (m *Record) GetValue() string { + if m != nil { + return m.Value + } + return "" +} + +func (m *Record) GetMetadata() map[string]string { + if m != nil { + return m.Metadata + } + return nil +} + +func init() { + proto.RegisterType((*HealthRequest)(nil), "HealthRequest") + proto.RegisterType((*HealthResponse)(nil), "HealthResponse") + proto.RegisterType((*StatsRequest)(nil), "StatsRequest") + proto.RegisterType((*StatsResponse)(nil), "StatsResponse") + proto.RegisterType((*LogRequest)(nil), "LogRequest") + proto.RegisterType((*Record)(nil), "Record") + proto.RegisterMapType((map[string]string)(nil), "Record.MetadataEntry") +} + +func init() { + proto.RegisterFile("micro/go-micro/debug/service/proto/debug.proto", fileDescriptor_dea322649cde1ef2) +} + +var fileDescriptor_dea322649cde1ef2 = []byte{ + // 399 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x52, 0xc1, 0x6e, 0xd4, 0x30, + 0x10, 0xdd, 0xac, 0x9b, 0xb4, 0x3b, 0x65, 0x17, 0x64, 0x15, 0x64, 0xad, 0x90, 0xa8, 0x7c, 0x0a, + 0x42, 0x78, 0xa1, 0x5c, 0x10, 0x5c, 0x41, 0xe2, 0x50, 0x2e, 0xe6, 0x0b, 0xdc, 0x64, 0x94, 0x2e, + 0x34, 0x71, 0xb0, 0x27, 0x95, 0xf6, 0xc4, 0xb7, 0x70, 0xe7, 0x23, 0x51, 0x6c, 0x2f, 0x6d, 0x44, + 0xa5, 0xbd, 0xcd, 0x7b, 0xf3, 0xf4, 0x3c, 0x33, 0x7e, 0xa0, 0xda, 0x6d, 0xe5, 0xec, 0xa6, 0xb1, + 0xaf, 0x63, 0x51, 0xe3, 0xd5, 0xd0, 0x6c, 0x3c, 0xba, 0xdb, 0x6d, 0x85, 0x9b, 0xde, 0x59, 0x4a, + 0x9c, 0x0a, 0xb5, 0x7c, 0x09, 0xcb, 0x2f, 0x68, 0x6e, 0xe8, 0x5a, 0xe3, 0xcf, 0x01, 0x3d, 0x71, + 0x01, 0xc7, 0x49, 0x2d, 0xb2, 0xf3, 0xac, 0x5c, 0xe8, 0x3d, 0x94, 0x25, 0xac, 0xf6, 0x52, 0xdf, + 0xdb, 0xce, 0x23, 0x7f, 0x06, 0x85, 0x27, 0x43, 0x83, 0x4f, 0xd2, 0x84, 0x64, 0x09, 0x8f, 0xbe, + 0x91, 0x21, 0x7f, 0xd8, 0xf3, 0x77, 0x06, 0xcb, 0x24, 0x4d, 0x9e, 0xcf, 0x61, 0x41, 0xdb, 0x16, + 0x3d, 0x99, 0xb6, 0x0f, 0xea, 0x23, 0x7d, 0x47, 0x04, 0x27, 0x32, 0x8e, 0xb0, 0x16, 0xf3, 0xd0, + 0xdb, 0xc3, 0x71, 0x96, 0xa1, 0x1f, 0x85, 0x82, 0x85, 0x46, 0x42, 0x23, 0xdf, 0x62, 0x6b, 0xdd, + 0x4e, 0x1c, 0x45, 0x3e, 0xa2, 0xd1, 0x89, 0xae, 0x1d, 0x9a, 0xda, 0x8b, 0x3c, 0x3a, 0x25, 0xc8, + 0x57, 0x30, 0x6f, 0x2a, 0x51, 0x04, 0x72, 0xde, 0x54, 0xf2, 0x3b, 0xc0, 0xa5, 0x6d, 0x0e, 0xee, + 0x12, 0xaf, 0xe1, 0xd0, 0xb4, 0x61, 0xb4, 0x13, 0x9d, 0x10, 0x3f, 0x83, 0xbc, 0xb2, 0x43, 0x47, + 0x61, 0x30, 0xa6, 0x23, 0x18, 0x59, 0xbf, 0xed, 0x2a, 0x0c, 0x63, 0x31, 0x1d, 0x81, 0xfc, 0x93, + 0x41, 0xa1, 0xb1, 0xb2, 0xae, 0xfe, 0xff, 0x10, 0xec, 0xfe, 0x21, 0xce, 0x20, 0xbf, 0x35, 0x37, + 0x03, 0x86, 0xb7, 0x16, 0x3a, 0x02, 0xfe, 0x16, 0x4e, 0x5a, 0x24, 0x53, 0x1b, 0x32, 0x82, 0x9d, + 0xb3, 0xf2, 0xf4, 0xe2, 0xa9, 0x8a, 0x76, 0xea, 0x6b, 0xe2, 0x3f, 0x77, 0xe4, 0x76, 0xfa, 0x9f, + 0x6c, 0xfd, 0x11, 0x96, 0x93, 0x16, 0x7f, 0x02, 0xec, 0x07, 0xee, 0xd2, 0x72, 0x63, 0xf9, 0xf0, + 0x5b, 0x1f, 0xe6, 0xef, 0xb3, 0x8b, 0x5f, 0x90, 0x7f, 0x1a, 0xc3, 0xc4, 0x5f, 0x41, 0x11, 0xb3, + 0xc1, 0x57, 0x6a, 0x92, 0xa7, 0xf5, 0x63, 0x35, 0x0d, 0x8d, 0x9c, 0xf1, 0x12, 0xf2, 0xf0, 0xe7, + 0x7c, 0xa9, 0xee, 0xc7, 0x64, 0xbd, 0x52, 0x93, 0x28, 0xc8, 0x19, 0x7f, 0x01, 0xec, 0xd2, 0x36, + 0xfc, 0x54, 0xdd, 0x7d, 0xc0, 0xfa, 0x38, 0x6d, 0x24, 0x67, 0x6f, 0xb2, 0xab, 0x22, 0xa4, 0xf8, + 0xdd, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xea, 0xdf, 0xa5, 0x1d, 0xf7, 0x02, 0x00, 0x00, +} diff --git a/debug/proto/debug.micro.go b/debug/service/proto/debug.pb.micro.go similarity index 60% rename from debug/proto/debug.micro.go rename to debug/service/proto/debug.pb.micro.go index baa06cee..45a26bac 100644 --- a/debug/proto/debug.micro.go +++ b/debug/service/proto/debug.pb.micro.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: micro/go-micro/debug/proto/debug.proto +// source: micro/go-micro/debug/service/proto/debug.proto package debug @@ -36,6 +36,7 @@ var _ server.Option type DebugService interface { Health(ctx context.Context, in *HealthRequest, opts ...client.CallOption) (*HealthResponse, error) Stats(ctx context.Context, in *StatsRequest, opts ...client.CallOption) (*StatsResponse, error) + Log(ctx context.Context, in *LogRequest, opts ...client.CallOption) (Debug_LogService, error) } type debugService struct { @@ -76,17 +77,63 @@ func (c *debugService) Stats(ctx context.Context, in *StatsRequest, opts ...clie return out, nil } +func (c *debugService) Log(ctx context.Context, in *LogRequest, opts ...client.CallOption) (Debug_LogService, error) { + req := c.c.NewRequest(c.name, "Debug.Log", &LogRequest{}) + stream, err := c.c.Stream(ctx, req, opts...) + if err != nil { + return nil, err + } + if err := stream.Send(in); err != nil { + return nil, err + } + return &debugServiceLog{stream}, nil +} + +type Debug_LogService interface { + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Recv() (*Record, error) +} + +type debugServiceLog struct { + stream client.Stream +} + +func (x *debugServiceLog) Close() error { + return x.stream.Close() +} + +func (x *debugServiceLog) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *debugServiceLog) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *debugServiceLog) Recv() (*Record, error) { + m := new(Record) + err := x.stream.Recv(m) + if err != nil { + return nil, err + } + return m, nil +} + // Server API for Debug service type DebugHandler interface { Health(context.Context, *HealthRequest, *HealthResponse) error Stats(context.Context, *StatsRequest, *StatsResponse) error + Log(context.Context, *LogRequest, Debug_LogStream) error } func RegisterDebugHandler(s server.Server, hdlr DebugHandler, opts ...server.HandlerOption) error { type debug interface { Health(ctx context.Context, in *HealthRequest, out *HealthResponse) error Stats(ctx context.Context, in *StatsRequest, out *StatsResponse) error + Log(ctx context.Context, stream server.Stream) error } type Debug struct { debug @@ -106,3 +153,38 @@ func (h *debugHandler) Health(ctx context.Context, in *HealthRequest, out *Healt func (h *debugHandler) Stats(ctx context.Context, in *StatsRequest, out *StatsResponse) error { return h.DebugHandler.Stats(ctx, in, out) } + +func (h *debugHandler) Log(ctx context.Context, stream server.Stream) error { + m := new(LogRequest) + if err := stream.Recv(m); err != nil { + return err + } + return h.DebugHandler.Log(ctx, m, &debugLogStream{stream}) +} + +type Debug_LogStream interface { + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Send(*Record) error +} + +type debugLogStream struct { + stream server.Stream +} + +func (x *debugLogStream) Close() error { + return x.stream.Close() +} + +func (x *debugLogStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *debugLogStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *debugLogStream) Send(m *Record) error { + return x.stream.Send(m) +} diff --git a/debug/service/proto/debug.proto b/debug/service/proto/debug.proto new file mode 100644 index 00000000..4f639245 --- /dev/null +++ b/debug/service/proto/debug.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; + +service Debug { + rpc Health(HealthRequest) returns (HealthResponse) {}; + rpc Stats(StatsRequest) returns (StatsResponse) {}; + rpc Log(LogRequest) returns (stream Record) {}; +} + +message HealthRequest { + // optional service name + string service = 1; +} + +message HealthResponse { + // default: ok + string status = 1; +} + +message StatsRequest { + // optional service name + string service = 1; +} + +message StatsResponse { + // timestamp of recording + uint64 timestamp = 1; + // unix timestamp + uint64 started = 2; + // in seconds + uint64 uptime = 3; + // in bytes + uint64 memory = 4; + // num threads + uint64 threads = 5; + // total gc in nanoseconds + uint64 gc = 6; +} + +// LogRequest requests service logs +message LogRequest { + // service to request logs for + string service = 1; + // stream records continuously + bool stream = 2; + // count of records to request + int64 count = 3; + // relative time in seconds + // before the current time + // from which to show logs + int64 since = 4; +} + +// Record is service log record +message Record { + // timestamp of log record + int64 timestamp = 1; + // record value + string value = 2; + // record metadata + map metadata = 3; +} diff --git a/debug/service/service.go b/debug/service/service.go new file mode 100644 index 00000000..d48b0a3d --- /dev/null +++ b/debug/service/service.go @@ -0,0 +1,86 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/micro/go-micro/client" + + "github.com/micro/go-micro/debug/log" + pb "github.com/micro/go-micro/debug/service/proto" +) + +// Debug provides debug service client +type Debug struct { + dbg pb.DebugService +} + +// NewDebug provides Debug service implementation +func NewDebug(name string) *Debug { + // create default client + cli := client.DefaultClient + + return &Debug{ + dbg: pb.NewDebugService(name, cli), + } +} + +// Logs queries the service logs and returns a channel to read the logs from +func (d *Debug) Log(opts ...log.ReadOption) (<-chan log.Record, error) { + options := log.ReadOptions{} + // initialize the read options + for _, o := range opts { + o(&options) + } + + req := &pb.LogRequest{} + if !options.Since.IsZero() { + req.Since = options.Since.Unix() + } + + if options.Count > 0 { + req.Count = int64(options.Count) + } + + req.Stream = options.Stream + + // get the log stream + stream, err := d.dbg.Log(context.Background(), req) + if err != nil { + return nil, fmt.Errorf("failed getting log stream: %s", err) + } + + // log channel for streaming logs + logChan := make(chan log.Record) + // go stream logs + go d.streamLogs(logChan, stream) + + return logChan, nil +} + +func (d *Debug) streamLogs(logChan chan log.Record, stream pb.Debug_LogService) { + defer stream.Close() + + for { + resp, err := stream.Recv() + if err != nil { + break + } + + metadata := make(map[string]string) + for k, v := range resp.Metadata { + metadata[k] = v + } + + record := log.Record{ + Timestamp: time.Unix(resp.Timestamp, 0), + Value: resp.Value, + Metadata: metadata, + } + + logChan <- record + } + + close(logChan) +} diff --git a/debug/stats/default.go b/debug/stats/default.go new file mode 100644 index 00000000..b2d78fce --- /dev/null +++ b/debug/stats/default.go @@ -0,0 +1,38 @@ +package stats + +import ( + "github.com/micro/go-micro/debug/buffer" +) + +type stats struct { + buffer *buffer.Buffer +} + +func (s *stats) Read() ([]*Stat, error) { + // TODO adjustable size and optional read values + buf := s.buffer.Get(1) + var stats []*Stat + + for _, b := range buf { + stat, ok := b.Value.(*Stat) + if !ok { + continue + } + stats = append(stats, stat) + } + + return stats, nil +} + +func (s *stats) Write(stat *Stat) error { + s.buffer.Put(stat) + return nil +} + +// NewStats returns a new in memory stats buffer +// TODO add options +func NewStats() Stats { + return &stats{ + buffer: buffer.New(1024), + } +} diff --git a/debug/stats/stats.go b/debug/stats/stats.go new file mode 100644 index 00000000..c24bfc42 --- /dev/null +++ b/debug/stats/stats.go @@ -0,0 +1,26 @@ +// Package stats provides runtime stats +package stats + +// Stats provides stats interface +type Stats interface { + // Read stat snapshot + Read() ([]*Stat, error) + // Write a stat snapshot + Write(*Stat) error +} + +// A runtime stat +type Stat struct { + // Timestamp of recording + Timestamp int64 + // Start time as unix timestamp + Started int64 + // Uptime in seconds + Uptime int64 + // Memory usage in bytes + Memory uint64 + // Threads aka go routines + Threads uint64 + // Garbage collection in nanoseconds + GC uint64 +} diff --git a/errors/errors_test.go b/errors/errors_test.go index 5df084d2..757d275d 100644 --- a/errors/errors_test.go +++ b/errors/errors_test.go @@ -7,7 +7,7 @@ import ( func TestErrors(t *testing.T) { testData := []*Error{ - &Error{ + { Id: "test", Code: 500, Detail: "Internal server error", diff --git a/function_test.go b/function_test.go index a7d1b18d..51d80fc9 100644 --- a/function_test.go +++ b/function_test.go @@ -5,16 +5,16 @@ import ( "sync" "testing" - proto "github.com/micro/go-micro/debug/proto" + proto "github.com/micro/go-micro/debug/service/proto" "github.com/micro/go-micro/registry/memory" + "github.com/micro/go-micro/util/test" ) func TestFunction(t *testing.T) { var wg sync.WaitGroup wg.Add(1) - r := memory.NewRegistry() - r.(*memory.Registry).Services = testData + r := memory.NewRegistry(memory.Services(test.Data)) // create service fn := NewFunction( diff --git a/go.mod b/go.mod index b0f037a3..6b438e41 100644 --- a/go.mod +++ b/go.mod @@ -1,78 +1,48 @@ module github.com/micro/go-micro -go 1.12 +go 1.13 require ( - cloud.google.com/go v0.44.0 // indirect github.com/BurntSushi/toml v0.3.1 - github.com/Microsoft/go-winio v0.4.14 // indirect - github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect - github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect - github.com/armon/go-radix v1.0.0 // indirect github.com/beevik/ntp v0.2.0 github.com/bitly/go-simplejson v0.5.0 - github.com/bwmarrin/discordgo v0.19.0 - github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect + github.com/bwmarrin/discordgo v0.20.1 + github.com/coreos/etcd v3.3.17+incompatible github.com/forestgiant/sliceutil v0.0.0-20160425183142-94783f95db6c github.com/fsnotify/fsnotify v1.4.7 - github.com/fsouza/go-dockerclient v1.4.2 + github.com/fsouza/go-dockerclient v1.6.0 github.com/ghodss/yaml v1.0.0 - github.com/go-kit/kit v0.9.0 // indirect + github.com/go-acme/lego/v3 v3.1.0 github.com/go-log/log v0.1.0 - github.com/go-playground/locales v0.12.1 // indirect - github.com/go-playground/universal-translator v0.16.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect github.com/golang/protobuf v1.3.2 - github.com/google/go-cmp v0.3.1 // indirect - github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 // indirect github.com/google/uuid v1.1.1 github.com/gorilla/handlers v1.4.2 - github.com/gorilla/websocket v1.4.0 - github.com/hashicorp/consul/api v1.1.0 - github.com/hashicorp/go-immutable-radix v1.1.0 // indirect - github.com/hashicorp/go-msgpack v0.5.5 // indirect - github.com/hashicorp/go-retryablehttp v0.5.4 // indirect - github.com/hashicorp/go-rootcerts v1.0.1 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-version v1.2.0 // indirect - github.com/hashicorp/golang-lru v0.5.3 // indirect + github.com/gorilla/websocket v1.4.1 github.com/hashicorp/hcl v1.0.0 - github.com/hashicorp/mdns v1.0.1 // indirect - github.com/hashicorp/memberlist v0.1.4 - github.com/hashicorp/serf v0.8.3 // indirect - github.com/imdario/mergo v0.3.7 + github.com/imdario/mergo v0.3.8 github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1 - github.com/json-iterator/go v1.1.7 - github.com/kisielk/errcheck v1.2.0 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect - github.com/leodido/go-urn v1.1.0 // indirect - github.com/lucas-clemente/quic-go v0.12.0 - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/json-iterator/go v1.1.8 + github.com/kr/pretty v0.1.0 + github.com/leodido/go-urn v1.2.0 // indirect + github.com/lib/pq v1.2.0 + github.com/lucas-clemente/quic-go v0.13.1 + github.com/mholt/certmagic v0.8.3 github.com/micro/cli v0.2.0 github.com/micro/mdns v0.3.0 - github.com/miekg/dns v1.1.15 // indirect - github.com/mitchellh/gox v1.0.1 // indirect + github.com/micro/protoc-gen-micro v1.0.0 // indirect + github.com/miekg/dns v1.1.22 github.com/mitchellh/hashstructure v1.0.0 - github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/nats-io/nats.go v1.8.1 - github.com/nats-io/nkeys v0.1.0 // indirect - github.com/nlopes/slack v0.5.0 - github.com/olekukonko/tablewriter v0.0.1 - github.com/onsi/ginkgo v1.8.0 // indirect - github.com/onsi/gomega v1.5.0 // indirect + github.com/nats-io/nats.go v1.9.1 + github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249 + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c github.com/pkg/errors v0.8.1 - github.com/posener/complete v1.2.1 // indirect - github.com/prometheus/client_golang v1.1.0 // indirect - github.com/sirupsen/logrus v1.4.2 // indirect - github.com/technoweenie/multipartstreamer v1.0.1 // indirect - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 - golang.org/x/mobile v0.0.0-20190806162312-597adff16ade // indirect - golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 - golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e // indirect - golang.org/x/tools v0.0.0-20190809145639-6d4652c779c4 // indirect - google.golang.org/grpc v1.22.1 - gopkg.in/go-playground/validator.v9 v9.29.1 + github.com/stretchr/testify v1.4.0 + go.uber.org/zap v1.12.0 // indirect + golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a + golang.org/x/net v0.0.0-20191109021931-daa7c04131f5 + google.golang.org/grpc v1.25.1 + gopkg.in/go-playground/validator.v9 v9.30.0 gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/telegram-bot-api.v4 v4.6.4 - honnef.co/go/tools v0.0.1-2019.2.2 // indirect ) diff --git a/go.sum b/go.sum index c01a6c5e..8a137787 100644 --- a/go.sum +++ b/go.sum @@ -1,276 +1,274 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= -cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= -cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= -cloud.google.com/go v0.44.0/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= +github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= +github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw= +github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= +github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= +github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= +github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.13/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20191101173118-65519b62243c/go.mod h1:7xhjOwRV2+0HXGmM0jxaEu+ZiXJFoVZOTfL/dmqbrD8= +github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k= +github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ= +github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/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-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= -github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= -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/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw= github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bwmarrin/discordgo v0.19.0 h1:kMED/DB0NR1QhRcalb85w0Cu3Ep2OrGAqZH1R5awQiY= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bwmarrin/discordgo v0.19.0/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/bwmarrin/discordgo v0.20.1 h1:Ihh3/mVoRwy3otmaoPDUioILBJq4fdWkpsi83oj2Lmk= +github.com/bwmarrin/discordgo v0.20.1/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= +github.com/cloudflare/cloudflare-go v0.10.2 h1:VBodKICVPnwmDxstcW3biKcDSpFIfS/RELUXsZSBYK4= +github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.17+incompatible h1:f/Z3EoDSx1yjaIjLQGo1diYUlQYSBrrAQ5vP8NjwXwo= +github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decker502/dnspod-go v0.2.0/go.mod h1:qsurYu1FgxcDwfSwXJdLt4kRsBLZeosEb9uq4Sy+08g= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 h1:dmUn0SuGx7unKFwxyeQ/oLUHhEfZosEDrpmYM+6MTuc= -github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v1.4.2-0.20190710153559-aa8249ae1b8b h1:+Ga+YpCDpcY1fln6GI0fiiirpqHGcob5/Vk3oKNuGdU= github.com/docker/docker v1.4.2-0.20190710153559-aa8249ae1b8b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20191101170500-ac7306503d23 h1:oqgGT9O61YAYvI41EBsLePOr+LE6roB0xY4gpkZuFSE= +github.com/docker/docker v1.4.2-0.20191101170500-ac7306503d23/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo= -github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/forestgiant/sliceutil v0.0.0-20160425183142-94783f95db6c h1:pBgVXWDXju1m8W4lnEeIqTHPOzhTUO81a7yknM/xQR4= github.com/forestgiant/sliceutil v0.0.0-20160425183142-94783f95db6c/go.mod h1:pFdJbAhRf7rh6YYMUdIQGyzne6zYL1tCUW8QV2B3UfY= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsouza/go-dockerclient v1.4.1 h1:W7wuJ3IB48WYZv/UBk9dCTIb9oX805+L9KIm65HcUYs= -github.com/fsouza/go-dockerclient v1.4.1/go.mod h1:PUNHxbowDqRXfRgZqMz1OeGtbWC6VKyZvJ99hDjB0qs= -github.com/fsouza/go-dockerclient v1.4.2 h1:dl6GfIWS5Qn4C6OfSnnoe6YuOV8lvKAE8W/YD1Q7udo= -github.com/fsouza/go-dockerclient v1.4.2/go.mod h1:COunfLZrsdwX/j3YVDAG8gIw3KutrI0x1+vGEJ5zxdI= +github.com/fsouza/go-dockerclient v1.4.4/go.mod h1:PrwszSL5fbmsESocROrOGq/NULMXRw+bajY0ltzD6MA= +github.com/fsouza/go-dockerclient v1.6.0 h1:f7j+AX94143JL1H3TiqSMkM4EcLDI0De1qD4GGn3Hig= +github.com/fsouza/go-dockerclient v1.6.0/go.mod h1:YWwtNPuL4XTX1SKJQk86cWPmmqwx+4np9qfPbb+znGc= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/gliderlabs/ssh v0.1.3/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-acme/lego v2.7.2+incompatible h1:ThhpPBgf6oa9X/vRd0kEmWOsX7+vmYdckmGZSb+FEp0= +github.com/go-acme/lego/v3 v3.1.0 h1:yanYFoYW8azFkCvJfIk7edWWfjkYkhDxe45ZsxoW4Xk= +github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE= +github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 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/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= -github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/handlers v1.4.1 h1:BHvcRGJe/TrL+OqFxoKQGddTgeibiOjaBssV5a/N9sw= -github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -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/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= -github.com/hashicorp/go-cleanhttp v0.5.1/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-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= -github.com/hashicorp/go-immutable-radix v1.1.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-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= -github.com/hashicorp/go-msgpack v0.5.5/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-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -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-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/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-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -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/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -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/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.1.4 h1:gkyML/r71w3FL8gUi74Vk76avkj/9lYAY9lvg0OcoGs= -github.com/hashicorp/memberlist v0.1.4/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/hashicorp/serf v0.8.3 h1:MWYcmct5EtKz0efYooPcL0yNkem+7kWxqXDi/UIh+8k= -github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd h1:anPrsicrIi2ColgWTVPk+TrN42hJIWlfPHSBP9S0ZkM= +github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd/go.mod h1:3LVOLeyx9XVvwPgrt2be44XgSqndprz1G18rSk8KD84= -github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= -github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1 h1:lnrOS18wZBYrzdDmnUeg1OVk+kQ3rxG8mZWU89DpMIA= github.com/joncalhoun/qson v0.0.0-20170526102502-8a9cab3a62b1/go.mod h1:DFXrEwSRX0p/aSvxE21319menCBFeQO0jXpRj7LEZUA= -github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= -github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c h1:VAx3LRNjVNvjtgO7KFRuT/3aye/0zJvwn01rHSfoolo= -github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= +github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/lucas-clemente/quic-go v0.7.1-0.20190710050138-1441923ab031 h1:wjcGvgllMOQw8wNYFH6acq/KlTAdjKMSo1EUYybHXto= -github.com/lucas-clemente/quic-go v0.7.1-0.20190710050138-1441923ab031/go.mod h1:lb5aAxL68VvhZ00e7yYuQVK/9FLggtYy4qo7oI5qzqA= -github.com/lucas-clemente/quic-go v0.11.2 h1:Mop0ac3zALaBR3wGs6j8OYe/tcFvFsxTUFMkE/7yUOI= -github.com/lucas-clemente/quic-go v0.11.2/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= -github.com/lucas-clemente/quic-go v0.12.0 h1:dYHUyB50gEQlK3KqytmNySzuyzAcaQ3iuI2ZReAfVrE= -github.com/lucas-clemente/quic-go v0.12.0/go.mod h1:UXJJPE4RfFef/xPO5wQm0tITK8gNfqwTxjbE7s3Vb8s= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA= +github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ= +github.com/lucas-clemente/quic-go v0.12.1/go.mod h1:UXJJPE4RfFef/xPO5wQm0tITK8gNfqwTxjbE7s3Vb8s= +github.com/lucas-clemente/quic-go v0.13.1 h1:CxtJTXQIh2aboCPk0M6vf530XOov6DZjVBiSE3nSj8s= +github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU= +github.com/marten-seemann/chacha20 v0.2.0 h1:f40vqzzx+3GdOmzQoItkLX5WLvHgPgyYqFFIO5Gh4hQ= +github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE= github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI= -github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= -github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= -github.com/marten-seemann/qtls v0.2.4 h1:mCJ6i1jAqcsm9XODrSGvXECodoAb1STta+TkxJCwCnE= -github.com/marten-seemann/qtls v0.2.4/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= -github.com/marten-seemann/qtls v0.3.1 h1:ySYIvhFjFY2JsNHY6VACvomMEDy3EvdPA6yciUFAiHw= -github.com/marten-seemann/qtls v0.3.1/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= -github.com/marten-seemann/qtls v0.3.2 h1:O7awy4bHEzSX/K3h+fZig3/Vo03s/RxlxgsAk9sYamI= github.com/marten-seemann/qtls v0.3.2/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/marten-seemann/qtls v0.4.1 h1:YlT8QP3WCCvvok7MGEZkMldXbyqgr8oFg5/n8Gtbkks= +github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mholt/certmagic v0.7.5/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ= +github.com/mholt/certmagic v0.8.3 h1:JOUiX9IAZbbgyjNP2GY6v/6lorH+9GkZsc7ktMpGCSo= +github.com/mholt/certmagic v0.8.3/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ= github.com/micro/cli v0.2.0 h1:ut3rV5JWqZjsXIa2MvGF+qMUP8DAUTvHX9Br5gO4afA= github.com/micro/cli v0.2.0/go.mod h1:jRT9gmfVKWSS6pkKcXQ8YhUyj6bzwxK8Fp5b0Y7qNnk= -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/mdns v0.1.1-0.20190729112526-ef68c9635478 h1:L6jnZZ763dMLlvst8P0dWHa1WbUu7ppUY1q3AY2hhIU= -github.com/micro/mdns v0.1.1-0.20190729112526-ef68c9635478/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc= -github.com/micro/mdns v0.2.0 h1:/+/n2PSiJURrXsBIGtfCz0hex/XYKqVsn51GAGdFrOE= -github.com/micro/mdns v0.2.0/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc= +github.com/micro/go-micro v1.16.0/go.mod h1:A0F58bHLh2m0LAI9QyhvmbN8c1cxhAZo3cM6s+iDsrM= github.com/micro/mdns v0.3.0 h1:bYycYe+98AXR3s8Nq5qvt6C573uFTDPIYzJemWON0QE= github.com/micro/mdns v0.3.0/go.mod h1:KJ0dW7KmicXU2BV++qkLlmHYcVv7/hHnbtguSWt9Aoc= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM= +github.com/micro/protoc-gen-micro v1.0.0 h1:qKh5S3I1RfenhIs5mqDFJLwRlRDlgin7XWiUKZbpwLM= +github.com/micro/protoc-gen-micro v1.0.0/go.mod h1:C8ij4DJhapBmypcT00AXdb0cZ675/3PqUO02buWWqbE= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA= -github.com/miekg/dns v1.1.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI= github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc= +github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -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/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4= +github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= 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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -278,125 +276,168 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/nats.go v1.8.1 h1:6lF/f1/NN6kzUDBz6pyvQDEXO39jqXcWRLu/tKjtOUQ= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= +github.com/nats-io/jwt v0.3.0 h1:xdnzwFETV++jNc4W1mw//qFyJGb2ABOombmZJQS4+Qo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats-server/v2 v2.1.0/go.mod h1:r5y0WgCag0dTj/qiHkHrXAcKQ/f5GMOZaEGdoxxnJ4I= github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= -github.com/nats-io/nkeys v0.0.2 h1:+qM7QpgXnvDDixitZtQUBDY9w/s9mu1ghS+JIbsrx6M= +github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0= -github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= -github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nlopes/slack v0.6.0 h1:jt0jxVQGhssx1Ib7naAOZEZcGdtIhTzkP0nopK0AsRA= +github.com/nlopes/slack v0.6.0/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= +github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249 h1:Pr5gZa2VcmktVwq0lyC39MsN5tz356vC/pQHKvq+QBo= +github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= +github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw= +github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ= +github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= +github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 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/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= -github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= +github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY= +github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 h1:IcSOAf4PyMp3U3XbIEj1/xJ2BjNN2jWv7JoyOsMxXUU= -golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a h1:R/qVym5WAxsZWQqZCwDY/8sdVKV1m1WgU4/S5IRQAzc= +golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190627132806-fd42eb6b336f/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190618124811-92942e4437e2/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20190711165009-e47acb2ca7f9/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20190806162312-597adff16ade/go.mod h1:AlhUtkH4DA4asiFC5RgK7ZKmauvtkAVcy9L0epCzlWo= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -404,18 +445,15 @@ golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190607181551-461777fb6f67 h1:rJJxsykSlULwd2P2+pg/rtnwN2FrWp4IuCxOSyS0V00= -golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191109021931-daa7c04131f5 h1:bHNaocaoJxYBo5cw41UyTMLjYlb8wPY7+WFrnklbHOM= +golang.org/x/net v0.0.0-20191109021931-daa7c04131f5/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -423,123 +461,104 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190621062556-bf70e4678053 h1:T0MJjz97TtCXa3ZNW2Oenb3KQWB91K965zMEbIJ4ThA= -golang.org/x/sys v0.0.0-20190621062556-bf70e4678053/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542 h1:6ZQFf1D2YYDDI7eSwW8adlkkavTB9sw5I24FVtEvNUQ= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190710143415-6ec70d6a5542/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M= -golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e h1:TsjK5I7fXk8f2FQrgu6NS7i5Qih3knl2FL1htyguLRE= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 h1:Hynbrlo6LbYI3H1IqXpkVDOcX/3HiPdhVEuyj5a59RM= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w= +golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190530171427-2b03ca6e44eb/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190620191750-1fa568393b23/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190710184609-286818132824/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190807201305-8be58fba6352/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190809145639-6d4652c779c4/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601 h1:9VBRTdmgQxbs6HE0sUnMrSWNePppAJU07NYvX5dIB04= -google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 h1:5pOB7se0B2+IssELuQUs6uoBgYJenkU2AQlvopc2sRw= -google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64 h1:iKtrH9Y8mcbADOP0YFaEMth7OfuHY9xHOwNj4znpM1A= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-playground/validator.v9 v9.29.0 h1:5ofssLNYgAA/inWn6rTZ4juWpRJUwEnXc1LG2IeXwgQ= -gopkg.in/go-playground/validator.v9 v9.29.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo= -gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= -gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek= -gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= -gopkg.in/src-d/go-billy.v4 v4.3.1 h1:OkK1DmefDy1Z6Veu82wdNj/cLpYORhdX4qdaYCPwc7s= -gopkg.in/src-d/go-billy.v4 v4.3.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.30.0 h1:Wk0Z37oBmKj9/n+tPyBHZmeL19LaCoK3Qq48VwYENss= +gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= +gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.11.0 h1:cJwWgJ0DXifrNrXM6RGN1Y2yR60Rr1zQ9Q5DX5S9qgU= -gopkg.in/src-d/go-git.v4 v4.11.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= -gopkg.in/src-d/go-git.v4 v4.12.0 h1:CKgvBCJCcdfNnyXPYI4Cp8PaDDAmAPEN0CtfEdEAbd8= -gopkg.in/src-d/go-git.v4 v4.12.0/go.mod h1:zjlNnzc1Wjn43v3Mtii7RVxiReNP0fIu9npcXKzuNp4= gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/telegram-bot-api.v4 v4.6.4 h1:hpHWhzn4jTCsAJZZ2loNKfy2QWyPDRJVl3aTFXeMW8g= @@ -547,14 +566,16 @@ gopkg.in/telegram-bot-api.v4 v4.6.4/go.mod h1:5DpGO5dbumb40px+dXcwCpcjmeHNYLpk0b gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190614002413-cb51c254f01b/go.mod h1:JlmFZigtG9vBVR3QGIQ9g/Usz4BzH+Xm6Z8iHQWRYUw= -honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/metadata/metadata.go b/metadata/metadata.go index 3ae6ba85..74b0aa3c 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -12,6 +12,7 @@ type metaKey struct{} // from Transport headers. type Metadata map[string]string +// Copy makes a copy of the metadata func Copy(md Metadata) Metadata { cmd := make(Metadata) for k, v := range md { @@ -20,11 +21,41 @@ func Copy(md Metadata) Metadata { return cmd } +// Get returns a single value from metadata in the context +func Get(ctx context.Context, key string) (string, bool) { + md, ok := FromContext(ctx) + if !ok { + return "", ok + } + val, ok := md[key] + return val, ok +} + +// FromContext returns metadata from the given context func FromContext(ctx context.Context) (Metadata, bool) { md, ok := ctx.Value(metaKey{}).(Metadata) return md, ok } +// NewContext creates a new context with the given metadata func NewContext(ctx context.Context, md Metadata) context.Context { return context.WithValue(ctx, metaKey{}, md) } + +// MergeContext merges metadata to existing metadata, overwriting if specified +func MergeContext(ctx context.Context, patchMd Metadata, overwrite bool) context.Context { + md, _ := ctx.Value(metaKey{}).(Metadata) + cmd := make(Metadata) + for k, v := range md { + cmd[k] = v + } + for k, v := range patchMd { + if _, ok := cmd[k]; ok && !overwrite { + // skip + } else { + cmd[k] = v + } + } + return context.WithValue(ctx, metaKey{}, cmd) + +} diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 9b5545eb..3706d14c 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -2,6 +2,7 @@ package metadata import ( "context" + "reflect" "testing" ) @@ -40,3 +41,42 @@ func TestMetadataContext(t *testing.T) { t.Errorf("Expected metadata length 1 got %d", i) } } + +func TestMergeContext(t *testing.T) { + type args struct { + existing Metadata + append Metadata + overwrite bool + } + tests := []struct { + name string + args args + want Metadata + }{ + { + name: "matching key, overwrite false", + args: args{ + existing: Metadata{"foo": "bar", "sumo": "demo"}, + append: Metadata{"sumo": "demo2"}, + overwrite: false, + }, + want: Metadata{"foo": "bar", "sumo": "demo"}, + }, + { + name: "matching key, overwrite true", + args: args{ + existing: Metadata{"foo": "bar", "sumo": "demo"}, + append: Metadata{"sumo": "demo2"}, + overwrite: true, + }, + want: Metadata{"foo": "bar", "sumo": "demo2"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got, _ := FromContext(MergeContext(NewContext(context.TODO(), tt.args.existing), tt.args.append, tt.args.overwrite)); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MergeContext() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/micro.go b/micro.go index 44bc52fc..295c08c5 100644 --- a/micro.go +++ b/micro.go @@ -14,11 +14,19 @@ type serviceKey struct{} // within go-micro. Its a convenience method for building // and initialising services. type Service interface { + // The service name + Name() string + // Init initialises options Init(...Option) + // Options returns the current options Options() Options + // Client is used to call services Client() client.Client + // Server is for handling requests and events Server() server.Server + // Run the service Run() error + // The service implementation String() string } diff --git a/monitor/default.go b/monitor/default.go index a03a0b31..b37b5658 100644 --- a/monitor/default.go +++ b/monitor/default.go @@ -7,7 +7,7 @@ import ( "time" "github.com/micro/go-micro/client" - pb "github.com/micro/go-micro/debug/proto" + pb "github.com/micro/go-micro/debug/service/proto" "github.com/micro/go-micro/registry" "github.com/micro/go-micro/registry/cache" ) @@ -78,13 +78,6 @@ func (m *monitor) check(service string) (*Status, error) { client.WithRetries(3), ) if err != nil { - // reap the dead node - m.registry.Deregister(®istry.Service{ - Name: service.Name, - Version: service.Version, - Nodes: []*registry.Node{node}, - }) - // save the error gerr = err continue @@ -140,7 +133,7 @@ func (m *monitor) reap() { defer m.Unlock() // range over our watched services - for service, _ := range m.services { + for service := range m.services { // check if the service exists in the registry if !serviceMap[service] { // if not, delete it in our status map @@ -195,14 +188,14 @@ func (m *monitor) run() { serviceMap := make(map[string]bool) m.RLock() - for service, _ := range m.services { + for service := range m.services { serviceMap[service] = true } m.RUnlock() go func() { // check the status of all watched services - for service, _ := range serviceMap { + for service := range serviceMap { select { case <-m.exit: return @@ -307,15 +300,13 @@ func (m *monitor) Stop() error { return nil default: close(m.exit) - for s, _ := range m.services { + for s := range m.services { delete(m.services, s) } m.registry.Stop() m.running = false return nil } - - return nil } func newMonitor(opts ...Option) Monitor { diff --git a/network/default.go b/network/default.go index 770f7f0f..dfc73bd2 100644 --- a/network/default.go +++ b/network/default.go @@ -4,20 +4,26 @@ import ( "errors" "fmt" "hash/fnv" + "io" + "math" + "sort" "sync" "time" "github.com/golang/protobuf/proto" "github.com/micro/go-micro/client" rtr "github.com/micro/go-micro/client/selector/router" - pbNet "github.com/micro/go-micro/network/proto" + "github.com/micro/go-micro/network/resolver/dns" + pbNet "github.com/micro/go-micro/network/service/proto" "github.com/micro/go-micro/proxy" "github.com/micro/go-micro/router" - pbRtr "github.com/micro/go-micro/router/proto" + pbRtr "github.com/micro/go-micro/router/service/proto" "github.com/micro/go-micro/server" "github.com/micro/go-micro/transport" "github.com/micro/go-micro/tunnel" + bun "github.com/micro/go-micro/tunnel/broker" tun "github.com/micro/go-micro/tunnel/transport" + "github.com/micro/go-micro/util/backoff" "github.com/micro/go-micro/util/log" ) @@ -28,11 +34,15 @@ var ( ControlChannel = "control" // DefaultLink is default network link DefaultLink = "network" + // MaxConnections is the max number of network client connections + MaxConnections = 3 ) var ( // ErrClientNotFound is returned when client for tunnel channel could not be found ErrClientNotFound = errors.New("client not found") + // ErrPeerLinkNotFound is returned when peer link could not be found in tunnel Links + ErrPeerLinkNotFound = errors.New("peer link not found") ) // network implements Network interface @@ -54,12 +64,26 @@ type network struct { // tunClient is a map of tunnel clients keyed over tunnel channel names tunClient map[string]transport.Client + // peerLinks is a map of links for each peer + peerLinks map[string]tunnel.Link sync.RWMutex // connected marks the network as connected connected bool // closed closes the network closed chan bool + // whether we've discovered by the network + discovered chan bool + // solicted checks whether routes were solicited by one node + solicited chan *node +} + +// message is network message +type message struct { + // msg is transport message + msg *transport.Message + // session is tunnel session + session tunnel.Session } // newNetwork returns a new network node @@ -70,15 +94,32 @@ func newNetwork(opts ...Option) Network { o(&options) } + // set the address to a hashed address + hasher := fnv.New64() + hasher.Write([]byte(options.Address + options.Id)) + address := fmt.Sprintf("%d", hasher.Sum64()) + + // set the address to advertise + var advertise string + var peerAddress string + + if len(options.Advertise) > 0 { + advertise = options.Advertise + peerAddress = options.Advertise + } else { + advertise = options.Address + peerAddress = address + } + // init tunnel address to the network bind address options.Tunnel.Init( tunnel.Address(options.Address), - tunnel.Nodes(options.Peers...), ) // init router Id to the network id options.Router.Init( router.Id(options.Id), + router.Address(peerAddress), ) // create tunnel client with tunnel transport @@ -86,23 +127,24 @@ func newNetwork(opts ...Option) Network { tun.WithTunnel(options.Tunnel), ) - // set the address to advertise - address := options.Address - if len(options.Advertise) > 0 { - address = options.Advertise - } + // create the tunnel broker + tunBroker := bun.NewBroker( + bun.WithTunnel(options.Tunnel), + ) // server is network server server := server.NewServer( server.Id(options.Id), - server.Address(options.Id), - server.Advertise(address), + server.Address(peerAddress), + server.Advertise(advertise), server.Name(options.Name), server.Transport(tunTransport), + server.Broker(tunBroker), ) // client is network client client := client.NewClient( + client.Broker(tunBroker), client.Transport(tunTransport), client.Selector( rtr.NewSelector( @@ -114,16 +156,19 @@ func newNetwork(opts ...Option) Network { network := &network{ node: &node{ id: options.Id, - address: address, + address: peerAddress, peers: make(map[string]*node), }, - options: options, - router: options.Router, - proxy: options.Proxy, - tunnel: options.Tunnel, - server: server, - client: client, - tunClient: make(map[string]transport.Client), + options: options, + router: options.Router, + proxy: options.Proxy, + tunnel: options.Tunnel, + server: server, + client: client, + tunClient: make(map[string]transport.Client), + peerLinks: make(map[string]tunnel.Link), + discovered: make(chan bool, 1), + solicited: make(chan *node, 1), } network.node.network = network @@ -131,6 +176,18 @@ func newNetwork(opts ...Option) Network { return network } +func (n *network) Init(opts ...Option) error { + n.Lock() + defer n.Unlock() + + // TODO: maybe only allow reinit of certain opts + for _, o := range opts { + o(&n.options) + } + + return nil +} + // Options returns network options func (n *network) Options() Options { n.RLock() @@ -146,86 +203,28 @@ func (n *network) Name() string { return n.options.Name } -// resolveNodes resolves network nodes to addresses -func (n *network) resolveNodes() ([]string, error) { - // resolve the network address to network nodes - records, err := n.options.Resolver.Resolve(n.options.Name) - if err != nil { - return nil, err - } - - nodeMap := make(map[string]bool) - - // collect network node addresses - var nodes []string - for _, record := range records { - nodes = append(nodes, record.Address) - nodeMap[record.Address] = true - } - - // append seed nodes if we have them - for _, node := range n.options.Peers { - if _, ok := nodeMap[node]; !ok { - nodes = append(nodes, node) - } - } - - return nodes, nil -} - -// resolve continuously resolves network nodes and initializes network tunnel with resolved addresses -func (n *network) resolve() { - resolve := time.NewTicker(ResolveTime) - defer resolve.Stop() - - for { - select { - case <-n.closed: - return - case <-resolve.C: - nodes, err := n.resolveNodes() - if err != nil { - log.Debugf("Network failed to resolve nodes: %v", err) - continue - } - // initialize the tunnel - n.tunnel.Init( - tunnel.Nodes(nodes...), - ) - } - } -} - -// handleNetConn handles network announcement messages -func (n *network) handleNetConn(sess tunnel.Session, msg chan *transport.Message) { - for { - m := new(transport.Message) - if err := sess.Recv(m); err != nil { - log.Debugf("Network tunnel [%s] receive error: %v", NetworkChannel, err) - return - } - - select { - case msg <- m: - case <-n.closed: - return - } - } -} - // acceptNetConn accepts connections from NetworkChannel -func (n *network) acceptNetConn(l tunnel.Listener, recv chan *transport.Message) { +func (n *network) acceptNetConn(l tunnel.Listener, recv chan *message) { + var i int for { // accept a connection conn, err := l.Accept() if err != nil { - // TODO: handle this - log.Debugf("Network tunnel [%s] accept error: %v", NetworkChannel, err) - return + sleep := backoff.Do(i) + log.Debugf("Network tunnel [%s] accept error: %v, backing off for %v", ControlChannel, err, sleep) + time.Sleep(sleep) + if i > 5 { + i = 0 + } + i++ + continue } select { case <-n.closed: + if err := conn.Close(); err != nil { + log.Debugf("Network tunnel [%s] failed to close connection: %v", NetworkChannel, err) + } return default: // go handle NetworkChannel connection @@ -234,10 +233,484 @@ func (n *network) acceptNetConn(l tunnel.Listener, recv chan *transport.Message) } } +// acceptCtrlConn accepts connections from ControlChannel +func (n *network) acceptCtrlConn(l tunnel.Listener, recv chan *message) { + var i int + for { + // accept a connection + conn, err := l.Accept() + if err != nil { + sleep := backoff.Do(i) + log.Debugf("Network tunnel [%s] accept error: %v, backing off for %v", ControlChannel, err, sleep) + time.Sleep(sleep) + if i > 5 { + // reset the counter + i = 0 + } + i++ + continue + } + + select { + case <-n.closed: + if err := conn.Close(); err != nil { + log.Debugf("Network tunnel [%s] failed to close connection: %v", ControlChannel, err) + } + return + default: + // go handle ControlChannel connection + go n.handleCtrlConn(conn, recv) + } + } +} + +// handleCtrlConn handles ControlChannel connections +// advertise advertises routes to the network +func (n *network) advertise(advertChan <-chan *router.Advert) { + hasher := fnv.New64() + for { + select { + // process local adverts and randomly fire them at other nodes + case advert := <-advertChan: + // create a proto advert + var events []*pbRtr.Event + + for _, event := range advert.Events { + // the routes service address + address := event.Route.Address + + // only hash the address if we're advertising our own local routes + if event.Route.Router == advert.Id { + // hash the service before advertising it + hasher.Reset() + // routes for multiple instances of a service will be collapsed here. + // TODO: once we store labels in the table this may need to change + // to include the labels in case they differ but highly unlikely + hasher.Write([]byte(event.Route.Service + n.node.Address())) + address = fmt.Sprintf("%d", hasher.Sum64()) + } + // calculate route metric to advertise + metric := n.getRouteMetric(event.Route.Router, event.Route.Gateway, event.Route.Link) + // NOTE: we override Gateway, Link and Address here + route := &pbRtr.Route{ + Service: event.Route.Service, + Address: address, + Gateway: n.node.Address(), + Network: event.Route.Network, + Router: event.Route.Router, + Link: DefaultLink, + Metric: metric, + } + e := &pbRtr.Event{ + Type: pbRtr.EventType(event.Type), + Timestamp: event.Timestamp.UnixNano(), + Route: route, + } + events = append(events, e) + } + + msg := &pbRtr.Advert{ + Id: advert.Id, + Type: pbRtr.AdvertType(advert.Type), + Timestamp: advert.Timestamp.UnixNano(), + Events: events, + } + + // send the advert to all on the control channel + // since its not a solicitation + if advert.Type != router.Solicitation { + if err := n.sendMsg("advert", ControlChannel, msg); err != nil { + log.Debugf("Network failed to advertise routes: %v", err) + } + continue + } + + // it's a solication, someone asked for it + // so we're going to pick off the node and send it + select { + case peer := <-n.solicited: + // someone requested the route + n.sendTo("advert", ControlChannel, peer, msg) + default: + // send to all since we can't get anything + n.sendMsg("advert", ControlChannel, msg) + } + case <-n.closed: + return + } + } +} + +func (n *network) initNodes(startup bool) { + nodes, err := n.resolveNodes() + if err != nil && !startup { + log.Debugf("Network failed to resolve nodes: %v", err) + return + } + + // initialize the tunnel + log.Tracef("Network initialising nodes %+v\n", nodes) + + n.tunnel.Init( + tunnel.Nodes(nodes...), + ) +} + +// resolveNodes resolves network nodes to addresses +func (n *network) resolveNodes() ([]string, error) { + // resolve the network address to network nodes + records, err := n.options.Resolver.Resolve(n.options.Name) + if err != nil { + log.Debugf("Network failed to resolve nodes: %v", err) + } + + // sort by lowest priority + if err == nil { + sort.Slice(records, func(i, j int) bool { return records[i].Priority < records[j].Priority }) + } + + // keep processing + + nodeMap := make(map[string]bool) + + // collect network node addresses + //nolint:prealloc + var nodes []string + var i int + + for _, record := range records { + if _, ok := nodeMap[record.Address]; ok { + continue + } + + nodeMap[record.Address] = true + nodes = append(nodes, record.Address) + + i++ + + // break once MaxConnection nodes has been reached + if i == MaxConnections { + break + } + } + + // use the dns resolver to expand peers + dns := &dns.Resolver{} + + // append seed nodes if we have them + for _, node := range n.options.Nodes { + // resolve anything that looks like a host name + records, err := dns.Resolve(node) + if err != nil { + log.Debugf("Failed to resolve %v %v", node, err) + continue + } + + // add to the node map + for _, record := range records { + if _, ok := nodeMap[record.Address]; !ok { + nodes = append(nodes, record.Address) + } + } + } + + return nodes, nil +} + +// handleNetConn handles network announcement messages +func (n *network) handleNetConn(s tunnel.Session, msg chan *message) { + for { + m := new(transport.Message) + if err := s.Recv(m); err != nil { + log.Debugf("Network tunnel [%s] receive error: %v", NetworkChannel, err) + switch err { + case io.EOF, tunnel.ErrReadTimeout: + s.Close() + return + } + continue + } + + // check if peer is set + peer := m.Header["Micro-Peer"] + + // check who the message is intended for + if len(peer) > 0 && peer != n.options.Id { + continue + } + + select { + case msg <- &message{ + msg: m, + session: s, + }: + case <-n.closed: + return + } + } +} + +func (n *network) handleCtrlConn(s tunnel.Session, msg chan *message) { + for { + m := new(transport.Message) + if err := s.Recv(m); err != nil { + log.Debugf("Network tunnel [%s] receive error: %v", ControlChannel, err) + switch err { + case io.EOF, tunnel.ErrReadTimeout: + s.Close() + return + } + continue + } + + // check if peer is set + peer := m.Header["Micro-Peer"] + + // check who the message is intended for + if len(peer) > 0 && peer != n.options.Id { + continue + } + + select { + case msg <- &message{ + msg: m, + session: s, + }: + case <-n.closed: + return + } + } +} + +// getHopCount queries network graph and returns hop count for given router +// - Routes for local services have hop count 1 +// - Routes with ID of adjacent nodes have hop count 2 +// - Routes by peers of the advertiser have hop count 3 +// - Routes beyond node neighbourhood have hop count 4 +func (n *network) getHopCount(rtr string) int { + // make sure node.peers are not modified + n.node.RLock() + defer n.node.RUnlock() + + // we are the origin of the route + if rtr == n.options.Id { + return 1 + } + + // the route origin is our peer + if _, ok := n.peers[rtr]; ok { + return 10 + } + + // the route origin is the peer of our peer + for _, peer := range n.peers { + for id := range peer.peers { + if rtr == id { + return 100 + } + } + } + // otherwise we are three hops away + return 1000 +} + +// getRouteMetric calculates router metric and returns it +// Route metric is calculated based on link status and route hopd count +func (n *network) getRouteMetric(router string, gateway string, link string) int64 { + // set the route metric + n.RLock() + defer n.RUnlock() + + // local links are marked as 1 + if link == "local" && gateway == "" { + return 1 + } + + // local links from other gateways as 2 + if link == "local" && gateway != "" { + return 2 + } + + log.Tracef("Network looking up %s link to gateway: %s", link, gateway) + + // attempt to find link based on gateway address + lnk, ok := n.peerLinks[gateway] + if !ok { + log.Debugf("Network failed to find a link to gateway: %s", gateway) + // no link found so infinite metric returned + return math.MaxInt64 + } + + // calculating metric + + delay := lnk.Delay() + hops := n.getHopCount(router) + length := lnk.Length() + + // make sure delay is non-zero + if delay == 0 { + delay = 1 + } + + // make sure length is non-zero + if length == 0 { + log.Debugf("Link length is 0 %v %v", link, lnk.Length()) + length = 10e9 + } + + log.Tracef("Network calculated metric %v delay %v length %v distance %v", (delay*length*int64(hops))/10e6, delay, length, hops) + + return (delay * length * int64(hops)) / 10e6 +} + +// processCtrlChan processes messages received on ControlChannel +func (n *network) processCtrlChan(listener tunnel.Listener) { + defer listener.Close() + + // receive control message queue + recv := make(chan *message, 128) + + // accept ControlChannel cconnections + go n.acceptCtrlConn(listener, recv) + + for { + select { + case m := <-recv: + // switch on type of message and take action + switch m.msg.Header["Micro-Method"] { + case "advert": + pbRtrAdvert := &pbRtr.Advert{} + + if err := proto.Unmarshal(m.msg.Body, pbRtrAdvert); err != nil { + log.Debugf("Network fail to unmarshal advert message: %v", err) + continue + } + + // don't process your own messages + if pbRtrAdvert.Id == n.options.Id { + continue + } + + log.Debugf("Network received advert message from: %s", pbRtrAdvert.Id) + + // loookup advertising node in our peer topology + advertNode := n.node.GetPeerNode(pbRtrAdvert.Id) + if advertNode == nil { + // if we can't find the node in our topology (MaxDepth) we skipp prcessing adverts + log.Debugf("Network skipping advert message from unknown peer: %s", pbRtrAdvert.Id) + continue + } + + var events []*router.Event + + for _, event := range pbRtrAdvert.Events { + // we know the advertising node is not the origin of the route + if pbRtrAdvert.Id != event.Route.Router { + // if the origin router is not the advertising node peer + // we can't rule out potential routing loops so we bail here + if peer := advertNode.GetPeerNode(event.Route.Router); peer == nil { + log.Debugf("Network skipping advert message from peer: %s", pbRtrAdvert.Id) + continue + } + } + + route := router.Route{ + Service: event.Route.Service, + Address: event.Route.Address, + Gateway: event.Route.Gateway, + Network: event.Route.Network, + Router: event.Route.Router, + Link: event.Route.Link, + Metric: event.Route.Metric, + } + + // calculate route metric and add to the advertised metric + // we need to make sure we do not overflow math.MaxInt64 + metric := n.getRouteMetric(event.Route.Router, event.Route.Gateway, event.Route.Link) + log.Tracef("Network metric for router %s and gateway %s: %v", event.Route.Router, event.Route.Gateway, metric) + + // check we don't overflow max int 64 + if d := route.Metric + metric; d <= 0 { + // set to max int64 if we overflow + route.Metric = math.MaxInt64 + } else { + // set the combined value of metrics otherwise + route.Metric = d + } + + // create router event + e := &router.Event{ + Type: router.EventType(event.Type), + Timestamp: time.Unix(0, pbRtrAdvert.Timestamp), + Route: route, + } + events = append(events, e) + } + + // if no events are eligible for processing continue + if len(events) == 0 { + log.Tracef("Network no events to be processed by router: %s", n.options.Id) + continue + } + + // create an advert and process it + advert := &router.Advert{ + Id: pbRtrAdvert.Id, + Type: router.AdvertType(pbRtrAdvert.Type), + Timestamp: time.Unix(0, pbRtrAdvert.Timestamp), + TTL: time.Duration(pbRtrAdvert.Ttl), + Events: events, + } + + log.Tracef("Network router %s processing advert: %s", n.Id(), advert.Id) + if err := n.router.Process(advert); err != nil { + log.Debugf("Network failed to process advert %s: %v", advert.Id, err) + } + case "solicit": + pbRtrSolicit := &pbRtr.Solicit{} + if err := proto.Unmarshal(m.msg.Body, pbRtrSolicit); err != nil { + log.Debugf("Network fail to unmarshal solicit message: %v", err) + continue + } + + log.Debugf("Network received solicit message from: %s", pbRtrSolicit.Id) + + // ignore solicitation when requested by you + if pbRtrSolicit.Id == n.options.Id { + continue + } + + log.Tracef("Network router flushing routes for: %s", pbRtrSolicit.Id) + + // advertise all the routes when a new node has connected + if err := n.router.Solicit(); err != nil { + log.Debugf("Network failed to solicit routes: %s", err) + } + + peer := &node{ + id: pbRtrSolicit.Id, + link: m.msg.Header["Micro-Link"], + } + + // specify that someone solicited the route + select { + case n.solicited <- peer: + default: + // don't block + } + } + case <-n.closed: + return + } + } +} + // processNetChan processes messages received on NetworkChannel -func (n *network) processNetChan(client transport.Client, listener tunnel.Listener) { +func (n *network) processNetChan(listener tunnel.Listener) { + defer listener.Close() + // receive network message queue - recv := make(chan *transport.Message, 128) + recv := make(chan *message, 128) // accept NetworkChannel connections go n.acceptNetConn(listener, recv) @@ -246,71 +719,112 @@ func (n *network) processNetChan(client transport.Client, listener tunnel.Listen select { case m := <-recv: // switch on type of message and take action - switch m.Header["Micro-Method"] { + switch m.msg.Header["Micro-Method"] { case "connect": // mark the time the message has been received now := time.Now() pbNetConnect := &pbNet.Connect{} - if err := proto.Unmarshal(m.Body, pbNetConnect); err != nil { + + if err := proto.Unmarshal(m.msg.Body, pbNetConnect); err != nil { log.Debugf("Network tunnel [%s] connect unmarshal error: %v", NetworkChannel, err) continue } + // don't process your own messages if pbNetConnect.Node.Id == n.options.Id { continue } + log.Debugf("Network received connect message from: %s", pbNetConnect.Node.Id) + peer := &node{ id: pbNetConnect.Node.Id, address: pbNetConnect.Node.Address, + link: m.msg.Header["Micro-Link"], peers: make(map[string]*node), lastSeen: now, } + + // update peer links + + if err := n.updatePeerLinks(peer); err != nil { + log.Debugf("Network failed updating peer links: %s", err) + } + + // add peer to the list of node peers if err := n.node.AddPeer(peer); err == ErrPeerExists { - log.Debugf("Network peer exists, refreshing: %s", peer.id) + log.Tracef("Network peer exists, refreshing: %s", peer.id) // update lastSeen time for the existing node - if err := n.RefreshPeer(peer.id, now); err != nil { + if err := n.RefreshPeer(peer.id, peer.link, now); err != nil { log.Debugf("Network failed refreshing peer %s: %v", peer.id, err) } - continue } + + // we send the peer message because someone has sent connect + // and wants to know what's on the network. The faster we + // respond the faster we start to converge + // get node peers down to MaxDepth encoded in protobuf msg := PeersToProto(n.node, MaxDepth) + // advertise yourself to the network - if err := n.sendMsg("peer", msg, NetworkChannel); err != nil { + if err := n.sendTo("peer", NetworkChannel, peer, msg); err != nil { log.Debugf("Network failed to advertise peers: %v", err) } + // advertise all the routes when a new node has connected if err := n.router.Solicit(); err != nil { log.Debugf("Network failed to solicit routes: %s", err) } + + // specify that we're soliciting + select { + case n.solicited <- peer: + default: + // don't block + } case "peer": // mark the time the message has been received now := time.Now() pbNetPeer := &pbNet.Peer{} - if err := proto.Unmarshal(m.Body, pbNetPeer); err != nil { + + if err := proto.Unmarshal(m.msg.Body, pbNetPeer); err != nil { log.Debugf("Network tunnel [%s] peer unmarshal error: %v", NetworkChannel, err) continue } + // don't process your own messages if pbNetPeer.Node.Id == n.options.Id { continue } - log.Debugf("Network received peer message from: %s", pbNetPeer.Node.Id) + + log.Debugf("Network received peer message from: %s %s", pbNetPeer.Node.Id, pbNetPeer.Node.Address) + peer := &node{ id: pbNetPeer.Node.Id, address: pbNetPeer.Node.Address, + link: m.msg.Header["Micro-Link"], peers: make(map[string]*node), lastSeen: now, } + + // update peer links + + if err := n.updatePeerLinks(peer); err != nil { + log.Debugf("Network failed updating peer links: %s", err) + } + if err := n.node.AddPeer(peer); err == nil { // send a solicit message when discovering new peer msg := &pbRtr.Solicit{ Id: n.options.Id, } - if err := n.sendMsg("solicit", msg, ControlChannel); err != nil { + + // only solicit this peer + if err := n.sendTo("solicit", ControlChannel, peer, msg); err != nil { log.Debugf("Network failed to send solicit message: %s", err) } + continue // we're expecting any error to be ErrPeerExists } else if err != ErrPeerExists { @@ -318,39 +832,58 @@ func (n *network) processNetChan(client transport.Client, listener tunnel.Listen continue } - log.Debugf("Network peer exists, refreshing: %s", pbNetPeer.Node.Id) + log.Tracef("Network peer exists, refreshing: %s", pbNetPeer.Node.Id) + // update lastSeen time for the peer - if err := n.RefreshPeer(pbNetPeer.Node.Id, now); err != nil { + if err := n.RefreshPeer(pbNetPeer.Node.Id, peer.link, now); err != nil { log.Debugf("Network failed refreshing peer %s: %v", pbNetPeer.Node.Id, err) } // NOTE: we don't unpack MaxDepth toplogy peer = UnpackPeerTopology(pbNetPeer, now, MaxDepth-1) - log.Debugf("Network updating topology of node: %s", n.node.id) + log.Tracef("Network updating topology of node: %s", n.node.id) if err := n.node.UpdatePeer(peer); err != nil { log.Debugf("Network failed to update peers: %v", err) } + + // tell the connect loop that we've been discovered + // so it stops sending connect messages out + select { + case n.discovered <- true: + default: + // don't block here + } case "close": pbNetClose := &pbNet.Close{} - if err := proto.Unmarshal(m.Body, pbNetClose); err != nil { + if err := proto.Unmarshal(m.msg.Body, pbNetClose); err != nil { log.Debugf("Network tunnel [%s] close unmarshal error: %v", NetworkChannel, err) continue } + // don't process your own messages if pbNetClose.Node.Id == n.options.Id { continue } + log.Debugf("Network received close message from: %s", pbNetClose.Node.Id) + peer := &node{ id: pbNetClose.Node.Id, address: pbNetClose.Node.Address, } + if err := n.DeletePeerNode(peer.id); err != nil { log.Debugf("Network failed to delete node %s routes: %v", peer.id, err) } + if err := n.prunePeerRoutes(peer); err != nil { log.Debugf("Network failed pruning peer %s routes: %v", peer.id, err) } + + // delete peer from the peerLinks + n.Lock() + delete(n.peerLinks, pbNetClose.Node.Address) + n.Unlock() } case <-n.closed: return @@ -358,60 +891,9 @@ func (n *network) processNetChan(client transport.Client, listener tunnel.Listen } } -// sendMsg sends a message to the tunnel channel -func (n *network) sendMsg(method string, msg proto.Message, channel string) error { - body, err := proto.Marshal(msg) - if err != nil { - return err - } - // create transport message and chuck it down the pipe - m := transport.Message{ - Header: map[string]string{ - "Micro-Method": method, - }, - Body: body, - } - - // check if the channel client is initialized - n.RLock() - client, ok := n.tunClient[channel] - if !ok || client == nil { - n.RUnlock() - return ErrClientNotFound - } - n.RUnlock() - - log.Debugf("Network sending %s message from: %s", method, n.options.Id) - if err := client.Send(&m); err != nil { - return err - } - - return nil -} - -// announce announces node peers to the network -func (n *network) announce(client transport.Client) { - announce := time.NewTicker(AnnounceTime) - defer announce.Stop() - - for { - select { - case <-n.closed: - return - case <-announce.C: - msg := PeersToProto(n.node, MaxDepth) - // advertise yourself to the network - if err := n.sendMsg("peer", msg, NetworkChannel); err != nil { - log.Debugf("Network failed to advertise peers: %v", err) - continue - } - } - } -} - // pruneRoutes prunes routes return by given query -func (n *network) pruneRoutes(q router.Query) error { - routes, err := n.router.Table().Query(q) +func (n *network) pruneRoutes(q ...router.QueryOption) error { + routes, err := n.router.Table().Query(q...) if err != nil && err != router.ErrRouteNotFound { return err } @@ -428,370 +910,95 @@ func (n *network) pruneRoutes(q router.Query) error { // pruneNodeRoutes prunes routes that were either originated by or routable via given node func (n *network) prunePeerRoutes(peer *node) error { // lookup all routes originated by router - q := router.NewQuery( + q := []router.QueryOption{ router.QueryRouter(peer.id), - ) - if err := n.pruneRoutes(q); err != nil { + } + if err := n.pruneRoutes(q...); err != nil { return err } // lookup all routes routable via gw - q = router.NewQuery( - router.QueryGateway(peer.id), - ) - if err := n.pruneRoutes(q); err != nil { + q = []router.QueryOption{ + router.QueryGateway(peer.address), + } + if err := n.pruneRoutes(q...); err != nil { return err } return nil } -// prune deltes node peers that have not been seen for longer than PruneTime seconds -// prune also removes all the routes either originated by or routable by the stale nodes -func (n *network) prune() { +// manage the process of announcing to peers and prune any peer nodes that have not been +// seen for a period of time. Also removes all the routes either originated by or routable +//by the stale nodes. it also resolves nodes periodically and adds them to the tunnel +func (n *network) manage() { + announce := time.NewTicker(AnnounceTime) + defer announce.Stop() prune := time.NewTicker(PruneTime) defer prune.Stop() + resolve := time.NewTicker(ResolveTime) + defer resolve.Stop() for { select { case <-n.closed: return + case <-announce.C: + msg := PeersToProto(n.node, MaxDepth) + // advertise yourself to the network + if err := n.sendMsg("peer", NetworkChannel, msg); err != nil { + log.Debugf("Network failed to advertise peers: %v", err) + } case <-prune.C: - pruned := n.PruneStalePeerNodes(PruneTime) + pruned := n.PruneStalePeers(PruneTime) + for id, peer := range pruned { log.Debugf("Network peer exceeded prune time: %s", id) + + n.Lock() + delete(n.peerLinks, peer.address) + n.Unlock() + if err := n.prunePeerRoutes(peer); err != nil { log.Debugf("Network failed pruning peer %s routes: %v", id, err) } } - } - } -} -// handleCtrlConn handles ControlChannel connections -func (n *network) handleCtrlConn(sess tunnel.Session, msg chan *transport.Message) { - for { - m := new(transport.Message) - if err := sess.Recv(m); err != nil { - // TODO: should we bail here? - log.Debugf("Network tunnel advert receive error: %v", err) - return - } - - select { - case msg <- m: - case <-n.closed: - return - } - } -} - -// acceptCtrlConn accepts connections from ControlChannel -func (n *network) acceptCtrlConn(l tunnel.Listener, recv chan *transport.Message) { - for { - // accept a connection - conn, err := l.Accept() - if err != nil { - // TODO: handle this - log.Debugf("Network tunnel [%s] accept error: %v", ControlChannel, err) - return - } - - select { - case <-n.closed: - return - default: - // go handle ControlChannel connection - go n.handleCtrlConn(conn, recv) - } - } -} - -// setRouteMetric calculates metric of the route and updates it in place -// - Local route metric is 1 -// - Routes with ID of adjacent nodes are 10 -// - Routes by peers of the advertiser are 100 -// - Routes beyond your neighbourhood are 1000 -func (n *network) setRouteMetric(route *router.Route) { - // we are the origin of the route - if route.Router == n.options.Id { - route.Metric = 1 - return - } - - // check if the route origin is our peer - if _, ok := n.peers[route.Router]; ok { - route.Metric = 10 - return - } - - // check if the route origin is the peer of our peer - for _, peer := range n.peers { - for id := range peer.peers { - if route.Router == id { - route.Metric = 100 - return - } - } - } - - // the origin of the route is beyond our neighbourhood - route.Metric = 1000 -} - -// processCtrlChan processes messages received on ControlChannel -func (n *network) processCtrlChan(client transport.Client, listener tunnel.Listener) { - // receive control message queue - recv := make(chan *transport.Message, 128) - - // accept ControlChannel cconnections - go n.acceptCtrlConn(listener, recv) - - for { - select { - case m := <-recv: - // switch on type of message and take action - switch m.Header["Micro-Method"] { - case "advert": - pbRtrAdvert := &pbRtr.Advert{} - if err := proto.Unmarshal(m.Body, pbRtrAdvert); err != nil { - log.Debugf("Network fail to unmarshal advert message: %v", err) - continue - } - // don't process your own messages - if pbRtrAdvert.Id == n.options.Id { - continue - } - log.Debugf("Network received advert message from: %s", pbRtrAdvert.Id) - // loookup advertising node in our peer topology - advertNode := n.node.GetPeerNode(pbRtrAdvert.Id) - if advertNode == nil { - // if we can't find the node in our topology (MaxDepth) we skipp prcessing adverts - log.Debugf("Network skipping advert message from unknown peer: %s", pbRtrAdvert.Id) - continue - } - - var events []*router.Event - for _, event := range pbRtrAdvert.Events { - // we know the advertising node is not the origin of the route - if pbRtrAdvert.Id != event.Route.Router { - // if the origin router is not the advertising node peer - // we can't rule out potential routing loops so we bail here - if peer := advertNode.GetPeerNode(event.Route.Router); peer == nil { - log.Debugf("Network skipping advert message from peer: %s", pbRtrAdvert.Id) - continue - } - } - route := router.Route{ - Service: event.Route.Service, - Address: event.Route.Address, - Gateway: event.Route.Gateway, - Network: event.Route.Network, - Router: event.Route.Router, - Link: event.Route.Link, - Metric: int(event.Route.Metric), - } - // set the route metric - n.node.RLock() - n.setRouteMetric(&route) - n.node.RUnlock() - // throw away metric bigger than 1000 - if route.Metric > 1000 { - log.Debugf("Network route metric %d dropping node: %s", route.Metric, route.Router) - continue - } - // create router event - e := &router.Event{ - Type: router.EventType(event.Type), - Timestamp: time.Unix(0, pbRtrAdvert.Timestamp), - Route: route, - } - events = append(events, e) - } - // if no events are eligible for processing continue - if len(events) == 0 { - log.Debugf("Network no events to be processed by router: %s", n.options.Id) - continue - } - // create an advert and process it - advert := &router.Advert{ - Id: pbRtrAdvert.Id, - Type: router.AdvertType(pbRtrAdvert.Type), - Timestamp: time.Unix(0, pbRtrAdvert.Timestamp), - TTL: time.Duration(pbRtrAdvert.Ttl), - Events: events, - } - - log.Debugf("Network router %s processing advert: %s", n.Id(), advert.Id) - if err := n.router.Process(advert); err != nil { - log.Debugf("Network failed to process advert %s: %v", advert.Id, err) - } - case "solicit": - pbRtrSolicit := &pbRtr.Solicit{} - if err := proto.Unmarshal(m.Body, pbRtrSolicit); err != nil { - log.Debugf("Network fail to unmarshal solicit message: %v", err) - continue - } - log.Debugf("Network received solicit message from: %s", pbRtrSolicit.Id) - // ignore solicitation when requested by you - if pbRtrSolicit.Id == n.options.Id { - continue - } - log.Debugf("Network router flushing routes for: %s", pbRtrSolicit.Id) - // advertise all the routes when a new node has connected - if err := n.router.Solicit(); err != nil { - log.Debugf("Network failed to solicit routes: %s", err) - } - } - case <-n.closed: - return - } - } -} - -// advertise advertises routes to the network -func (n *network) advertise(client transport.Client, advertChan <-chan *router.Advert) { - hasher := fnv.New64() - for { - select { - // process local adverts and randomly fire them at other nodes - case advert := <-advertChan: - // create a proto advert - var events []*pbRtr.Event - for _, event := range advert.Events { - // the routes service address - address := event.Route.Address - - // only hash the address if we're advertising our own local routes - if event.Route.Router == advert.Id { - // hash the service before advertising it - hasher.Reset() - hasher.Write([]byte(event.Route.Address + n.node.id)) - address = fmt.Sprintf("%d", hasher.Sum64()) - } - - // NOTE: we override Gateway, Link and Address here - // TODO: should we avoid overriding gateway? - route := &pbRtr.Route{ - Service: event.Route.Service, - Address: address, - Gateway: n.node.id, - Network: event.Route.Network, - Router: event.Route.Router, - Link: DefaultLink, - Metric: int64(event.Route.Metric), - } - e := &pbRtr.Event{ - Type: pbRtr.EventType(event.Type), - Timestamp: event.Timestamp.UnixNano(), - Route: route, - } - events = append(events, e) - } - msg := &pbRtr.Advert{ - Id: advert.Id, - Type: pbRtr.AdvertType(advert.Type), - Timestamp: advert.Timestamp.UnixNano(), - Events: events, - } - if err := n.sendMsg("advert", msg, ControlChannel); err != nil { - log.Debugf("Network failed to advertise routes: %v", err) + // get a list of all routes + routes, err := n.options.Router.Table().List() + if err != nil { + log.Debugf("Network failed listing routes when pruning peers: %v", err) continue } - case <-n.closed: - return + + // collect all the router IDs in the routing table + routers := make(map[string]bool) + + for _, route := range routes { + // check if its been processed + if _, ok := routers[route.Router]; ok { + continue + } + + // mark as processed + routers[route.Router] = true + + // if the router is NOT in our peer graph, delete all routes originated by it + if peer := n.node.GetPeerNode(route.Router); peer != nil { + continue + } + + if err := n.pruneRoutes(router.QueryRouter(route.Router)); err != nil { + log.Debugf("Network failed deleting routes by %s: %v", route.Router, err) + } + } + case <-resolve.C: + n.initNodes(false) } } } -// Connect connects the network -func (n *network) Connect() error { - n.Lock() - // return if already connected - if n.connected { - n.Unlock() - return nil - } - - // try to resolve network nodes - nodes, err := n.resolveNodes() - if err != nil { - log.Debugf("Network failed to resolve nodes: %v", err) - } - - // connect network tunnel - if err := n.tunnel.Connect(); err != nil { - n.Unlock() - return err - } - - // set our internal node address - // if advertise address is not set - if len(n.options.Advertise) == 0 { - n.node.address = n.tunnel.Address() - n.server.Init(server.Advertise(n.tunnel.Address())) - } - - // initialize the tunnel to resolved nodes - n.tunnel.Init( - tunnel.Nodes(nodes...), - ) - - // dial into ControlChannel to send route adverts - ctrlClient, err := n.tunnel.Dial(ControlChannel, tunnel.DialMulticast()) - if err != nil { - n.Unlock() - return err - } - - n.tunClient[ControlChannel] = ctrlClient - - // listen on ControlChannel - ctrlListener, err := n.tunnel.Listen(ControlChannel) - if err != nil { - n.Unlock() - return err - } - - // dial into NetworkChannel to send network messages - netClient, err := n.tunnel.Dial(NetworkChannel, tunnel.DialMulticast()) - if err != nil { - n.Unlock() - return err - } - - n.tunClient[NetworkChannel] = netClient - - // listen on NetworkChannel - netListener, err := n.tunnel.Listen(NetworkChannel) - if err != nil { - n.Unlock() - return err - } - - // create closed channel - n.closed = make(chan bool) - - // start the router - if err := n.options.Router.Start(); err != nil { - n.Unlock() - return err - } - - // start advertising routes - advertChan, err := n.options.Router.Advertise() - if err != nil { - n.Unlock() - return err - } - - // start the server - if err := n.server.Start(); err != nil { - n.Unlock() - return err - } - n.Unlock() - +func (n *network) sendConnect() { // send connect message to NetworkChannel // NOTE: in theory we could do this as soon as // Dial to NetworkChannel succeeds, but instead @@ -802,26 +1009,277 @@ func (n *network) Connect() error { Address: n.node.address, }, } - if err := n.sendMsg("connect", msg, NetworkChannel); err != nil { + + if err := n.sendMsg("connect", NetworkChannel, msg); err != nil { log.Debugf("Network failed to send connect message: %s", err) } +} - // go resolving network nodes - go n.resolve() - // broadcast peers - go n.announce(netClient) - // prune stale nodes - go n.prune() - // listen to network messages - go n.processNetChan(netClient, netListener) - // advertise service routes - go n.advertise(ctrlClient, advertChan) - // accept and process routes - go n.processCtrlChan(ctrlClient, ctrlListener) +// sendTo sends a message to a specific node as a one off. +// we need this because when links die, we have no discovery info, +// and sending to an existing multicast link doesn't immediately work +func (n *network) sendTo(method, channel string, peer *node, msg proto.Message) error { + body, err := proto.Marshal(msg) + if err != nil { + return err + } + c, err := n.tunnel.Dial(channel, tunnel.DialMode(tunnel.Multicast), tunnel.DialLink(peer.link)) + if err != nil { + return err + } + defer c.Close() + log.Debugf("Network sending %s message from: %s to %s", method, n.options.Id, peer.id) + + return c.Send(&transport.Message{ + Header: map[string]string{ + "Micro-Method": method, + "Micro-Peer": peer.id, + }, + Body: body, + }) +} + +// sendMsg sends a message to the tunnel channel +func (n *network) sendMsg(method, channel string, msg proto.Message) error { + body, err := proto.Marshal(msg) + if err != nil { + return err + } + + // check if the channel client is initialized + n.RLock() + client, ok := n.tunClient[channel] + if !ok || client == nil { + n.RUnlock() + return ErrClientNotFound + } + n.RUnlock() + + log.Debugf("Network sending %s message from: %s", method, n.options.Id) + + return client.Send(&transport.Message{ + Header: map[string]string{ + "Micro-Method": method, + }, + Body: body, + }) +} + +// updatePeerLinks updates link for a given peer +func (n *network) updatePeerLinks(peer *node) error { n.Lock() + defer n.Unlock() + + linkId := peer.link + + log.Tracef("Network looking up link %s in the peer links", linkId) + + // lookup the peer link + var peerLink tunnel.Link + + for _, link := range n.tunnel.Links() { + if link.Id() == linkId { + peerLink = link + break + } + } + + if peerLink == nil { + return ErrPeerLinkNotFound + } + + // if the peerLink is found in the returned links update peerLinks + log.Tracef("Network updating peer links for peer %s", peer.address) + + // add peerLink to the peerLinks map + if link, ok := n.peerLinks[peer.address]; ok { + // if the existing has better Length then the new, replace it + if link.Length() < peerLink.Length() { + n.peerLinks[peer.address] = peerLink + } + } else { + n.peerLinks[peer.address] = peerLink + } + + return nil +} + +// connect will wait for a link to be established and send the connect +// message. We're trying to ensure convergence pretty quickly. So we want +// to hear back. In the case we become completely disconnected we'll +// connect again once a new link is established +func (n *network) connect() { + // discovered lets us know what we received a peer message back + var discovered bool + var attempts int + + // our advertise address + loopback := n.server.Options().Advertise + // actual address + address := n.tunnel.Address() + + for { + // connected is used to define if the link is connected + var connected bool + + // check the links state + for _, link := range n.tunnel.Links() { + // skip loopback + if link.Loopback() { + continue + } + + // if remote is ourselves + switch link.Remote() { + case loopback, address: + continue + } + + if link.State() == "connected" { + connected = true + break + } + } + + // if we're not connected wait + if !connected { + // reset discovered + discovered = false + // sleep for a second + time.Sleep(time.Second) + // now try again + continue + } + + // we're connected but are we discovered? + if !discovered { + // recreate the clients because all the tunnel links are gone + // so we haven't send discovery beneath + if err := n.createClients(); err != nil { + log.Debugf("Failed to recreate network/control clients: %v", err) + continue + } + + // send the connect message + n.sendConnect() + } + + // check if we've been discovered + select { + case <-n.discovered: + discovered = true + attempts = 0 + case <-n.closed: + return + case <-time.After(time.Second + backoff.Do(attempts)): + // we have to try again + attempts++ + + // reset attempts 5 == ~2mins + if attempts > 5 { + attempts = 0 + } + } + } +} + +// Connect connects the network +func (n *network) Connect() error { + n.Lock() + defer n.Unlock() + + // connect network tunnel + if err := n.tunnel.Connect(); err != nil { + return err + } + + // return if already connected + if n.connected { + // initialise the nodes + n.initNodes(false) + // send the connect message + go n.sendConnect() + return nil + } + + // initialise the nodes + n.initNodes(true) + + // set our internal node address + // if advertise address is not set + if len(n.options.Advertise) == 0 { + n.server.Init(server.Advertise(n.tunnel.Address())) + } + + // listen on NetworkChannel + netListener, err := n.tunnel.Listen( + NetworkChannel, + tunnel.ListenMode(tunnel.Multicast), + tunnel.ListenTimeout(AnnounceTime*2), + ) + if err != nil { + return err + } + + // listen on ControlChannel + ctrlListener, err := n.tunnel.Listen( + ControlChannel, + tunnel.ListenMode(tunnel.Multicast), + tunnel.ListenTimeout(router.AdvertiseTableTick*2), + ) + if err != nil { + return err + } + + // dial into ControlChannel to send route adverts + ctrlClient, err := n.tunnel.Dial(ControlChannel, tunnel.DialMode(tunnel.Multicast)) + if err != nil { + return err + } + + n.tunClient[ControlChannel] = ctrlClient + + // dial into NetworkChannel to send network messages + netClient, err := n.tunnel.Dial(NetworkChannel, tunnel.DialMode(tunnel.Multicast)) + if err != nil { + return err + } + + n.tunClient[NetworkChannel] = netClient + + // create closed channel + n.closed = make(chan bool) + + // start the router + if err := n.options.Router.Start(); err != nil { + return err + } + + // start advertising routes + advertChan, err := n.options.Router.Advertise() + if err != nil { + return err + } + + // start the server + if err := n.server.Start(); err != nil { + return err + } + + // advertise service routes + go n.advertise(advertChan) + // listen to network messages + go n.processNetChan(netListener) + // accept and process routes + go n.processCtrlChan(ctrlListener) + // manage connection once links are established + go n.connect() + // resolve nodes, broadcast announcements and prune stale nodes + go n.manage() + + // we're now connected n.connected = true - n.Unlock() return nil } @@ -845,6 +1303,40 @@ func (n *network) close() error { return nil } +// createClients is used to create new clients in the event we lose all the tunnels +func (n *network) createClients() error { + // dial into ControlChannel to send route adverts + ctrlClient, err := n.tunnel.Dial(ControlChannel, tunnel.DialMode(tunnel.Multicast)) + if err != nil { + return err + } + + // dial into NetworkChannel to send network messages + netClient, err := n.tunnel.Dial(NetworkChannel, tunnel.DialMode(tunnel.Multicast)) + if err != nil { + return err + } + + n.Lock() + defer n.Unlock() + + // set the control client + c, ok := n.tunClient[ControlChannel] + if ok { + c.Close() + } + n.tunClient[ControlChannel] = ctrlClient + + // set the network client + c, ok = n.tunClient[NetworkChannel] + if ok { + c.Close() + } + n.tunClient[NetworkChannel] = netClient + + return nil +} + // Close closes network connection func (n *network) Close() error { n.Lock() @@ -873,7 +1365,8 @@ func (n *network) Close() error { Address: n.node.address, }, } - if err := n.sendMsg("close", msg, NetworkChannel); err != nil { + + if err := n.sendMsg("close", NetworkChannel, msg); err != nil { log.Debugf("Network failed to send close message: %s", err) } } diff --git a/network/handler/handler.go b/network/handler/handler.go deleted file mode 100644 index 840d6ce5..00000000 --- a/network/handler/handler.go +++ /dev/null @@ -1,145 +0,0 @@ -// Package handler implements network RPC handler -package handler - -import ( - "context" - - "github.com/micro/go-micro/errors" - "github.com/micro/go-micro/network" - pbNet "github.com/micro/go-micro/network/proto" - pbRtr "github.com/micro/go-micro/router/proto" -) - -// Network implements network handler -type Network struct { - Network network.Network -} - -func flatten(n network.Node, visited map[string]bool) []network.Node { - // if node is nil runaway - if n == nil { - return nil - } - - // set visisted - if visited == nil { - visited = make(map[string]bool) - } - - // check if already visited - if visited[n.Id()] == true { - return nil - } - - // create new list of nodes - var nodes []network.Node - - // append the current node - nodes = append(nodes, n) - - // set to visited - visited[n.Id()] = true - - // visit the list of peers - for _, node := range n.Peers() { - nodes = append(nodes, flatten(node, visited)...) - } - - return nodes -} - -// Nodes returns the list of nodes -func (n *Network) Nodes(ctx context.Context, req *pbNet.NodesRequest, resp *pbNet.NodesResponse) error { - depth := uint(req.Depth) - if depth <= 0 || depth > network.MaxDepth { - depth = network.MaxDepth - } - - // root node - nodes := map[string]network.Node{} - - // get peers encoded into protobuf - peers := flatten(n.Network, nil) - - // walk all the peers - for _, peer := range peers { - if peer == nil { - continue - } - if _, ok := nodes[peer.Id()]; ok { - continue - } - - // add to visited list - nodes[n.Network.Id()] = peer - - resp.Nodes = append(resp.Nodes, &pbNet.Node{ - Id: peer.Id(), - Address: peer.Address(), - }) - } - - return nil -} - -// Graph returns the network graph from this root node -func (n *Network) Graph(ctx context.Context, req *pbNet.GraphRequest, resp *pbNet.GraphResponse) error { - depth := uint(req.Depth) - if depth <= 0 || depth > network.MaxDepth { - depth = network.MaxDepth - } - - // get peers encoded into protobuf - peers := network.PeersToProto(n.Network, depth) - - // set the root node - resp.Root = peers - - return nil -} - -// Routes returns a list of routing table routes -func (n *Network) Routes(ctx context.Context, req *pbNet.RoutesRequest, resp *pbNet.RoutesResponse) error { - routes, err := n.Network.Options().Router.Table().List() - if err != nil { - return errors.InternalServerError("go.micro.network", "failed to list routes: %s", err) - } - - var respRoutes []*pbRtr.Route - for _, route := range routes { - respRoute := &pbRtr.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int64(route.Metric), - } - respRoutes = append(respRoutes, respRoute) - } - - resp.Routes = respRoutes - - return nil -} - -// Services returns a list of services based on the routing table -func (n *Network) Services(ctx context.Context, req *pbNet.ServicesRequest, resp *pbNet.ServicesResponse) error { - routes, err := n.Network.Options().Router.Table().List() - if err != nil { - return errors.InternalServerError("go.micro.network", "failed to list services: %s", err) - } - - services := make(map[string]bool) - - for _, route := range routes { - if _, ok := services[route.Service]; ok { - continue - } - services[route.Service] = true - resp.Services = append(resp.Services, route.Service) - } - - return nil -} diff --git a/network/network.go b/network/network.go index ef303992..e927241b 100644 --- a/network/network.go +++ b/network/network.go @@ -38,6 +38,8 @@ type Node interface { type Network interface { // Node is network node Node + // Initialise options + Init(...Option) error // Options returns the network options Options() Options // Name of the network diff --git a/network/node.go b/network/node.go index d015b9d9..658c0964 100644 --- a/network/node.go +++ b/network/node.go @@ -6,7 +6,7 @@ import ( "sync" "time" - pb "github.com/micro/go-micro/network/proto" + pb "github.com/micro/go-micro/network/service/proto" ) var ( @@ -28,6 +28,8 @@ type node struct { id string // address is node address address string + // link on which we communicate with the peer + link string // peers are nodes with direct link to this node peers map[string]*node // network returns the node network @@ -127,7 +129,7 @@ func (n *node) UpdatePeer(peer *node) error { // RefreshPeer updates node timestamp // It returns false if the peer has not been found. -func (n *node) RefreshPeer(id string, now time.Time) error { +func (n *node) RefreshPeer(id, link string, now time.Time) error { n.Lock() defer n.Unlock() @@ -136,6 +138,9 @@ func (n *node) RefreshPeer(id string, now time.Time) error { return ErrPeerNotFound } + // set peer link + peer.link = link + if peer.lastSeen.Before(now) { peer.lastSeen = now } @@ -158,7 +163,7 @@ func (n *node) Nodes() []Node { visited := n.walk(untilNoMorePeers, justWalk) - var nodes []Node + nodes := make([]Node, 0, len(visited)) // collect all the nodes and return them for _, node := range visited { nodes = append(nodes, node) @@ -170,9 +175,6 @@ func (n *node) Nodes() []Node { // GetPeerNode returns a node from node MaxDepth topology // It returns nil if the peer was not found func (n *node) GetPeerNode(id string) *node { - n.RLock() - defer n.RUnlock() - // get node topology up to MaxDepth top := n.Topology(MaxDepth) @@ -219,7 +221,7 @@ func (n *node) DeletePeerNode(id string) error { // PruneStalePeerNodes prune the peers that have not been seen for longer than given time // It returns a map of the the nodes that got pruned -func (n *node) PruneStalePeerNodes(pruneTime time.Duration) map[string]*node { +func (n *node) PruneStalePeers(pruneTime time.Duration) map[string]*node { n.Lock() defer n.Unlock() @@ -240,12 +242,9 @@ func (n *node) PruneStalePeerNodes(pruneTime time.Duration) map[string]*node { return pruned } -// Topology returns a copy of the node topology down to given depth -// NOTE: the returned node is a node graph - not a single node -func (n *node) Topology(depth uint) *node { - n.RLock() - defer n.RUnlock() - +// getTopology traverses node graph and builds node topology +// NOTE: this function is not thread safe +func (n *node) getTopology(depth uint) *node { // make a copy of yourself node := &node{ id: n.id, @@ -265,7 +264,7 @@ func (n *node) Topology(depth uint) *node { // iterate through our peers and update the node peers for _, peer := range n.peers { - nodePeer := peer.Topology(depth) + nodePeer := peer.getTopology(depth) if _, ok := node.peers[nodePeer.id]; !ok { node.peers[nodePeer.id] = nodePeer } @@ -274,14 +273,23 @@ func (n *node) Topology(depth uint) *node { return node } +// Topology returns a copy of the node topology down to given depth +// NOTE: the returned node is a node graph - not a single node +func (n *node) Topology(depth uint) *node { + n.RLock() + defer n.RUnlock() + + return n.getTopology(depth) +} + // Peers returns node peers up to MaxDepth func (n *node) Peers() []Node { n.RLock() defer n.RUnlock() - var peers []Node + peers := make([]Node, 0, len(n.peers)) for _, nodePeer := range n.peers { - peer := nodePeer.Topology(MaxDepth) + peer := nodePeer.getTopology(MaxDepth) peers = append(peers, peer) } @@ -322,6 +330,11 @@ func peerProtoTopology(peer Node, depth uint) *pb.Peer { Address: peer.Address(), } + // set the network name if network is not nil + if peer.Network() != nil { + node.Network = peer.Network().Name() + } + pbPeers := &pb.Peer{ Node: node, Peers: make([]*pb.Peer, 0), @@ -351,6 +364,12 @@ func PeersToProto(node Node, depth uint) *pb.Peer { Id: node.Id(), Address: node.Address(), } + + // set the network name if network is not nil + if node.Network() != nil { + pbNode.Network = node.Network().Name() + } + // we will build proto topology into this pbPeers := &pb.Peer{ Node: pbNode, diff --git a/network/node_test.go b/network/node_test.go index 80a41c70..51c4988f 100644 --- a/network/node_test.go +++ b/network/node_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - pb "github.com/micro/go-micro/network/proto" + pb "github.com/micro/go-micro/network/service/proto" ) var ( @@ -225,7 +225,7 @@ func TestPruneStalePeerNodes(t *testing.T) { time.Sleep(pruneTime) // should delete all nodes besides node - pruned := node.PruneStalePeerNodes(pruneTime) + pruned := node.PruneStalePeers(pruneTime) if len(pruned) != len(nodes)-1 { t.Errorf("Expected pruned node count: %d, got: %d", len(nodes)-1, len(pruned)) diff --git a/network/options.go b/network/options.go index 0a3c5b2b..63ff6fe7 100644 --- a/network/options.go +++ b/network/options.go @@ -22,8 +22,8 @@ type Options struct { Address string // Advertise sets the address to advertise Advertise string - // Peers is a list of peers to connect to - Peers []string + // Nodes is a list of nodes to connect to + Nodes []string // Tunnel is network tunnel Tunnel tunnel.Tunnel // Router is network router @@ -62,10 +62,10 @@ func Advertise(a string) Option { } } -// Peers is a list of peers to connect to -func Peers(n ...string) Option { +// Nodes is a list of nodes to connect to +func Nodes(n ...string) Option { return func(o *Options) { - o.Peers = n + o.Nodes = n } } diff --git a/network/resolver/dns/dns.go b/network/resolver/dns/dns.go index 3cfa2746..a8d27b06 100644 --- a/network/resolver/dns/dns.go +++ b/network/resolver/dns/dns.go @@ -1,31 +1,78 @@ -// Package dns resolves names to dns srv records +// Package dns resolves names to dns records package dns import ( - "fmt" + "context" "net" "github.com/micro/go-micro/network/resolver" + "github.com/miekg/dns" ) // Resolver is a DNS network resolve -type Resolver struct{} +type Resolver struct { + // The resolver address to use + Address string +} // Resolve assumes ID is a domain name e.g micro.mu func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) { - _, addrs, err := net.LookupSRV("network", "udp", name) + host, port, err := net.SplitHostPort(name) + if err != nil { + host = name + port = "8085" + } + + if len(host) == 0 { + host = "localhost" + } + + if len(r.Address) == 0 { + r.Address = "1.0.0.1:53" + } + + //nolint:prealloc + var records []*resolver.Record + + // parsed an actual ip + if v := net.ParseIP(host); v != nil { + records = append(records, &resolver.Record{ + Address: net.JoinHostPort(host, port), + }) + return records, nil + } + + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(host), dns.TypeA) + rec, err := dns.ExchangeContext(context.Background(), m, r.Address) if err != nil { return nil, err } - var records []*resolver.Record - for _, addr := range addrs { - address := addr.Target - if addr.Port > 0 { - address = fmt.Sprintf("%s:%d", addr.Target, addr.Port) + + for _, answer := range rec.Answer { + h := answer.Header() + // check record type matches + if h.Rrtype != dns.TypeA { + continue } + + arec, _ := answer.(*dns.A) + addr := arec.A.String() + + // join resolved record with port + address := net.JoinHostPort(addr, port) + // append to record set records = append(records, &resolver.Record{ Address: address, }) } + + // no records returned so just best effort it + if len(records) == 0 { + records = append(records, &resolver.Record{ + Address: net.JoinHostPort(host, port), + }) + } + return records, nil } diff --git a/network/resolver/dnssrv/dnssrv.go b/network/resolver/dnssrv/dnssrv.go new file mode 100644 index 00000000..63a92eda --- /dev/null +++ b/network/resolver/dnssrv/dnssrv.go @@ -0,0 +1,31 @@ +// Package dns srv resolves names to dns srv records +package dnssrv + +import ( + "fmt" + "net" + + "github.com/micro/go-micro/network/resolver" +) + +// Resolver is a DNS network resolve +type Resolver struct{} + +// Resolve assumes ID is a domain name e.g micro.mu +func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) { + _, addrs, err := net.LookupSRV("network", "udp", name) + if err != nil { + return nil, err + } + records := make([]*resolver.Record, 0, len(addrs)) + for _, addr := range addrs { + address := addr.Target + if addr.Port > 0 { + address = fmt.Sprintf("%s:%d", addr.Target, addr.Port) + } + records = append(records, &resolver.Record{ + Address: address, + }) + } + return records, nil +} diff --git a/network/resolver/resolver.go b/network/resolver/resolver.go index 369269df..a6cd6b12 100644 --- a/network/resolver/resolver.go +++ b/network/resolver/resolver.go @@ -11,5 +11,6 @@ type Resolver interface { // A resolved record type Record struct { - Address string `json:"address"` + Address string `json:"address"` + Priority int64 `json:"priority"` } diff --git a/network/resolver/static/static.go b/network/resolver/static/static.go index 8157e4ea..485332c5 100644 --- a/network/resolver/static/static.go +++ b/network/resolver/static/static.go @@ -21,7 +21,7 @@ func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) { }, nil } - var records []*resolver.Record + records := make([]*resolver.Record, 0, len(r.Nodes)) for _, node := range r.Nodes { records = append(records, &resolver.Record{ diff --git a/network/proto/network.pb.go b/network/service/proto/network.pb.go similarity index 65% rename from network/proto/network.pb.go rename to network/service/proto/network.pb.go index 0b339d69..d94687f5 100644 --- a/network/proto/network.pb.go +++ b/network/service/proto/network.pb.go @@ -4,11 +4,9 @@ package go_micro_network import ( - context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" - proto1 "github.com/micro/go-micro/router/proto" - grpc "google.golang.org/grpc" + proto1 "github.com/micro/go-micro/router/service/proto" math "math" ) @@ -23,6 +21,148 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package +// Query is passed in a LookupRequest +type Query struct { + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Gateway string `protobuf:"bytes,3,opt,name=gateway,proto3" json:"gateway,omitempty"` + Router string `protobuf:"bytes,4,opt,name=router,proto3" json:"router,omitempty"` + Network string `protobuf:"bytes,5,opt,name=network,proto3" json:"network,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Query) Reset() { *m = Query{} } +func (m *Query) String() string { return proto.CompactTextString(m) } +func (*Query) ProtoMessage() {} +func (*Query) Descriptor() ([]byte, []int) { + return fileDescriptor_0b7953b26a7c4730, []int{0} +} + +func (m *Query) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Query.Unmarshal(m, b) +} +func (m *Query) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Query.Marshal(b, m, deterministic) +} +func (m *Query) XXX_Merge(src proto.Message) { + xxx_messageInfo_Query.Merge(m, src) +} +func (m *Query) XXX_Size() int { + return xxx_messageInfo_Query.Size(m) +} +func (m *Query) XXX_DiscardUnknown() { + xxx_messageInfo_Query.DiscardUnknown(m) +} + +var xxx_messageInfo_Query proto.InternalMessageInfo + +func (m *Query) GetService() string { + if m != nil { + return m.Service + } + return "" +} + +func (m *Query) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *Query) GetGateway() string { + if m != nil { + return m.Gateway + } + return "" +} + +func (m *Query) GetRouter() string { + if m != nil { + return m.Router + } + return "" +} + +func (m *Query) GetNetwork() string { + if m != nil { + return m.Network + } + return "" +} + +type ConnectRequest struct { + Nodes []*Node `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ConnectRequest) Reset() { *m = ConnectRequest{} } +func (m *ConnectRequest) String() string { return proto.CompactTextString(m) } +func (*ConnectRequest) ProtoMessage() {} +func (*ConnectRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_0b7953b26a7c4730, []int{1} +} + +func (m *ConnectRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ConnectRequest.Unmarshal(m, b) +} +func (m *ConnectRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ConnectRequest.Marshal(b, m, deterministic) +} +func (m *ConnectRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ConnectRequest.Merge(m, src) +} +func (m *ConnectRequest) XXX_Size() int { + return xxx_messageInfo_ConnectRequest.Size(m) +} +func (m *ConnectRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ConnectRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ConnectRequest proto.InternalMessageInfo + +func (m *ConnectRequest) GetNodes() []*Node { + if m != nil { + return m.Nodes + } + return nil +} + +type ConnectResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ConnectResponse) Reset() { *m = ConnectResponse{} } +func (m *ConnectResponse) String() string { return proto.CompactTextString(m) } +func (*ConnectResponse) ProtoMessage() {} +func (*ConnectResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_0b7953b26a7c4730, []int{2} +} + +func (m *ConnectResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ConnectResponse.Unmarshal(m, b) +} +func (m *ConnectResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ConnectResponse.Marshal(b, m, deterministic) +} +func (m *ConnectResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ConnectResponse.Merge(m, src) +} +func (m *ConnectResponse) XXX_Size() int { + return xxx_messageInfo_ConnectResponse.Size(m) +} +func (m *ConnectResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ConnectResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ConnectResponse proto.InternalMessageInfo + // PeerRequest requests list of peers type NodesRequest struct { // node topology depth @@ -36,7 +176,7 @@ func (m *NodesRequest) Reset() { *m = NodesRequest{} } func (m *NodesRequest) String() string { return proto.CompactTextString(m) } func (*NodesRequest) ProtoMessage() {} func (*NodesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{0} + return fileDescriptor_0b7953b26a7c4730, []int{3} } func (m *NodesRequest) XXX_Unmarshal(b []byte) error { @@ -77,7 +217,7 @@ func (m *NodesResponse) Reset() { *m = NodesResponse{} } func (m *NodesResponse) String() string { return proto.CompactTextString(m) } func (*NodesResponse) ProtoMessage() {} func (*NodesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{1} + return fileDescriptor_0b7953b26a7c4730, []int{4} } func (m *NodesResponse) XXX_Unmarshal(b []byte) error { @@ -117,7 +257,7 @@ func (m *GraphRequest) Reset() { *m = GraphRequest{} } func (m *GraphRequest) String() string { return proto.CompactTextString(m) } func (*GraphRequest) ProtoMessage() {} func (*GraphRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{2} + return fileDescriptor_0b7953b26a7c4730, []int{5} } func (m *GraphRequest) XXX_Unmarshal(b []byte) error { @@ -156,7 +296,7 @@ func (m *GraphResponse) Reset() { *m = GraphResponse{} } func (m *GraphResponse) String() string { return proto.CompactTextString(m) } func (*GraphResponse) ProtoMessage() {} func (*GraphResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{3} + return fileDescriptor_0b7953b26a7c4730, []int{6} } func (m *GraphResponse) XXX_Unmarshal(b []byte) error { @@ -185,6 +325,8 @@ func (m *GraphResponse) GetRoot() *Peer { } type RoutesRequest struct { + // filter based on + Query *Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -194,7 +336,7 @@ func (m *RoutesRequest) Reset() { *m = RoutesRequest{} } func (m *RoutesRequest) String() string { return proto.CompactTextString(m) } func (*RoutesRequest) ProtoMessage() {} func (*RoutesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{4} + return fileDescriptor_0b7953b26a7c4730, []int{7} } func (m *RoutesRequest) XXX_Unmarshal(b []byte) error { @@ -215,6 +357,13 @@ func (m *RoutesRequest) XXX_DiscardUnknown() { var xxx_messageInfo_RoutesRequest proto.InternalMessageInfo +func (m *RoutesRequest) GetQuery() *Query { + if m != nil { + return m.Query + } + return nil +} + type RoutesResponse struct { Routes []*proto1.Route `protobuf:"bytes,1,rep,name=routes,proto3" json:"routes,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -226,7 +375,7 @@ func (m *RoutesResponse) Reset() { *m = RoutesResponse{} } func (m *RoutesResponse) String() string { return proto.CompactTextString(m) } func (*RoutesResponse) ProtoMessage() {} func (*RoutesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{5} + return fileDescriptor_0b7953b26a7c4730, []int{8} } func (m *RoutesResponse) XXX_Unmarshal(b []byte) error { @@ -264,7 +413,7 @@ func (m *ServicesRequest) Reset() { *m = ServicesRequest{} } func (m *ServicesRequest) String() string { return proto.CompactTextString(m) } func (*ServicesRequest) ProtoMessage() {} func (*ServicesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{6} + return fileDescriptor_0b7953b26a7c4730, []int{9} } func (m *ServicesRequest) XXX_Unmarshal(b []byte) error { @@ -296,7 +445,7 @@ func (m *ServicesResponse) Reset() { *m = ServicesResponse{} } func (m *ServicesResponse) String() string { return proto.CompactTextString(m) } func (*ServicesResponse) ProtoMessage() {} func (*ServicesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{7} + return fileDescriptor_0b7953b26a7c4730, []int{10} } func (m *ServicesResponse) XXX_Unmarshal(b []byte) error { @@ -329,17 +478,21 @@ type Node struct { // node id Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // node address - Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + // the network + Network string `protobuf:"bytes,3,opt,name=network,proto3" json:"network,omitempty"` + // associated metadata + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *Node) Reset() { *m = Node{} } func (m *Node) String() string { return proto.CompactTextString(m) } func (*Node) ProtoMessage() {} func (*Node) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{8} + return fileDescriptor_0b7953b26a7c4730, []int{11} } func (m *Node) XXX_Unmarshal(b []byte) error { @@ -374,6 +527,20 @@ func (m *Node) GetAddress() string { return "" } +func (m *Node) GetNetwork() string { + if m != nil { + return m.Network + } + return "" +} + +func (m *Node) GetMetadata() map[string]string { + if m != nil { + return m.Metadata + } + return nil +} + // Connect is sent when the node connects to the network type Connect struct { // network mode @@ -387,7 +554,7 @@ func (m *Connect) Reset() { *m = Connect{} } func (m *Connect) String() string { return proto.CompactTextString(m) } func (*Connect) ProtoMessage() {} func (*Connect) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{9} + return fileDescriptor_0b7953b26a7c4730, []int{12} } func (m *Connect) XXX_Unmarshal(b []byte) error { @@ -428,7 +595,7 @@ func (m *Close) Reset() { *m = Close{} } func (m *Close) String() string { return proto.CompactTextString(m) } func (*Close) ProtoMessage() {} func (*Close) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{10} + return fileDescriptor_0b7953b26a7c4730, []int{13} } func (m *Close) XXX_Unmarshal(b []byte) error { @@ -471,7 +638,7 @@ func (m *Peer) Reset() { *m = Peer{} } func (m *Peer) String() string { return proto.CompactTextString(m) } func (*Peer) ProtoMessage() {} func (*Peer) Descriptor() ([]byte, []int) { - return fileDescriptor_0b7953b26a7c4730, []int{11} + return fileDescriptor_0b7953b26a7c4730, []int{14} } func (m *Peer) XXX_Unmarshal(b []byte) error { @@ -507,6 +674,9 @@ func (m *Peer) GetPeers() []*Peer { } func init() { + proto.RegisterType((*Query)(nil), "go.micro.network.Query") + proto.RegisterType((*ConnectRequest)(nil), "go.micro.network.ConnectRequest") + proto.RegisterType((*ConnectResponse)(nil), "go.micro.network.ConnectResponse") proto.RegisterType((*NodesRequest)(nil), "go.micro.network.NodesRequest") proto.RegisterType((*NodesResponse)(nil), "go.micro.network.NodesResponse") proto.RegisterType((*GraphRequest)(nil), "go.micro.network.GraphRequest") @@ -516,6 +686,7 @@ func init() { proto.RegisterType((*ServicesRequest)(nil), "go.micro.network.ServicesRequest") proto.RegisterType((*ServicesResponse)(nil), "go.micro.network.ServicesResponse") proto.RegisterType((*Node)(nil), "go.micro.network.Node") + proto.RegisterMapType((map[string]string)(nil), "go.micro.network.Node.MetadataEntry") proto.RegisterType((*Connect)(nil), "go.micro.network.Connect") proto.RegisterType((*Close)(nil), "go.micro.network.Close") proto.RegisterType((*Peer)(nil), "go.micro.network.Peer") @@ -526,210 +697,41 @@ func init() { } var fileDescriptor_0b7953b26a7c4730 = []byte{ - // 416 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x93, 0x5d, 0x6f, 0xda, 0x30, - 0x14, 0x86, 0x21, 0x10, 0x3e, 0xce, 0x16, 0x60, 0xd6, 0x34, 0x45, 0xb9, 0xd8, 0x98, 0xb5, 0x0b, - 0x34, 0x6d, 0x66, 0x02, 0x71, 0x35, 0x4d, 0x9a, 0xc4, 0x45, 0xa5, 0x4a, 0x45, 0x95, 0xf9, 0x03, - 0x85, 0xc4, 0x82, 0xa8, 0x25, 0x4e, 0x1d, 0xd3, 0xfe, 0xc2, 0xfe, 0xaf, 0xca, 0x1f, 0xe1, 0x33, - 0x41, 0xed, 0x1d, 0xe7, 0xf0, 0xf8, 0x3d, 0x3e, 0xaf, 0xdf, 0xc0, 0x64, 0x15, 0xcb, 0xf5, 0x76, - 0x49, 0x42, 0xbe, 0x19, 0x6e, 0xe2, 0x50, 0xf0, 0xe1, 0x8a, 0xff, 0x36, 0x3f, 0x12, 0x26, 0x9f, - 0xb9, 0xb8, 0x1f, 0xa6, 0x82, 0xcb, 0x5d, 0x45, 0x74, 0x85, 0x7a, 0x2b, 0x4e, 0x34, 0x45, 0x6c, - 0x3f, 0x18, 0x97, 0x0b, 0x09, 0xbe, 0x95, 0x4c, 0x58, 0x1d, 0x53, 0x18, 0x19, 0xfc, 0x03, 0x3e, - 0xce, 0x78, 0xc4, 0x32, 0xca, 0x1e, 0xb7, 0x2c, 0x93, 0xe8, 0x33, 0xb8, 0x11, 0x4b, 0xe5, 0xda, - 0xaf, 0xf6, 0xab, 0x03, 0x8f, 0x9a, 0x02, 0xff, 0x03, 0xcf, 0x52, 0x59, 0xca, 0x93, 0x8c, 0xa1, - 0x5f, 0xe0, 0x26, 0xaa, 0xe1, 0x57, 0xfb, 0xb5, 0xc1, 0x87, 0xd1, 0x17, 0x72, 0x7a, 0x1b, 0xa2, - 0x78, 0x6a, 0x20, 0x35, 0xe4, 0x4a, 0x2c, 0xd2, 0xf5, 0xe5, 0x21, 0x7f, 0xc1, 0xb3, 0x94, 0x1d, - 0xf2, 0x13, 0xea, 0x82, 0x73, 0xa9, 0xa9, 0xc2, 0x19, 0xb7, 0x8c, 0x09, 0xaa, 0x19, 0xdc, 0x05, - 0x8f, 0xaa, 0xbd, 0xf2, 0x45, 0xf0, 0x7f, 0xe8, 0xe4, 0x0d, 0x2b, 0x47, 0xa0, 0xa1, 0x57, 0x2f, - 0xb8, 0xb4, 0xb5, 0x44, 0x1f, 0xa0, 0x96, 0xc2, 0x9f, 0xa0, 0x3b, 0x67, 0xe2, 0x29, 0x0e, 0xf7, - 0xa2, 0x04, 0x7a, 0xfb, 0x96, 0x95, 0x0d, 0xa0, 0x95, 0xd9, 0x9e, 0x16, 0x6e, 0xd3, 0x5d, 0x8d, - 0xff, 0x40, 0x5d, 0xf9, 0x80, 0x3a, 0xe0, 0xc4, 0x91, 0xde, 0xa3, 0x4d, 0x9d, 0x38, 0x42, 0x3e, - 0x34, 0x17, 0x51, 0x24, 0x58, 0x96, 0xf9, 0x8e, 0x6e, 0xe6, 0x25, 0x9e, 0x40, 0x73, 0xca, 0x93, - 0x84, 0x85, 0x52, 0xad, 0xaf, 0xec, 0x2b, 0x5f, 0x5f, 0x5b, 0xac, 0x19, 0x3c, 0x06, 0x77, 0xfa, - 0xc0, 0x8d, 0x67, 0x6f, 0x3e, 0x74, 0x07, 0x75, 0xe5, 0xe0, 0x7b, 0xce, 0xa8, 0x87, 0x4f, 0x19, - 0x13, 0xea, 0xde, 0xb5, 0x0b, 0x8f, 0x62, 0xa0, 0xd1, 0x8b, 0x03, 0xcd, 0x99, 0xe9, 0xa3, 0x6b, - 0x70, 0xf5, 0xf3, 0xa2, 0xaf, 0xe7, 0x67, 0x0e, 0xd3, 0x11, 0x7c, 0x2b, 0xfd, 0xdf, 0x38, 0x8e, - 0x2b, 0x4a, 0x4b, 0xe7, 0xb1, 0x48, 0xeb, 0x30, 0xce, 0x45, 0x5a, 0x47, 0x41, 0xc6, 0x15, 0x74, - 0x03, 0x0d, 0x13, 0x14, 0x54, 0x00, 0x1f, 0x65, 0x2a, 0xe8, 0x97, 0x03, 0x3b, 0xb9, 0x39, 0xb4, - 0xf2, 0x88, 0xa0, 0xef, 0xe7, 0xfc, 0x49, 0xa2, 0x02, 0x7c, 0x09, 0xc9, 0x45, 0x97, 0x0d, 0xfd, - 0xb1, 0x8e, 0x5f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x3a, 0x63, 0x7a, 0x7b, 0x2c, 0x04, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// NetworkClient is the client API for Network service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type NetworkClient interface { - // Returns the entire network graph - Graph(ctx context.Context, in *GraphRequest, opts ...grpc.CallOption) (*GraphResponse, error) - // Returns a list of known nodes in the network - Nodes(ctx context.Context, in *NodesRequest, opts ...grpc.CallOption) (*NodesResponse, error) - // Returns a list of known routes in the network - Routes(ctx context.Context, in *RoutesRequest, opts ...grpc.CallOption) (*RoutesResponse, error) - // Returns a list of known services based on routes - Services(ctx context.Context, in *ServicesRequest, opts ...grpc.CallOption) (*ServicesResponse, error) -} - -type networkClient struct { - cc *grpc.ClientConn -} - -func NewNetworkClient(cc *grpc.ClientConn) NetworkClient { - return &networkClient{cc} -} - -func (c *networkClient) Graph(ctx context.Context, in *GraphRequest, opts ...grpc.CallOption) (*GraphResponse, error) { - out := new(GraphResponse) - err := c.cc.Invoke(ctx, "/go.micro.network.Network/Graph", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *networkClient) Nodes(ctx context.Context, in *NodesRequest, opts ...grpc.CallOption) (*NodesResponse, error) { - out := new(NodesResponse) - err := c.cc.Invoke(ctx, "/go.micro.network.Network/Nodes", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *networkClient) Routes(ctx context.Context, in *RoutesRequest, opts ...grpc.CallOption) (*RoutesResponse, error) { - out := new(RoutesResponse) - err := c.cc.Invoke(ctx, "/go.micro.network.Network/Routes", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *networkClient) Services(ctx context.Context, in *ServicesRequest, opts ...grpc.CallOption) (*ServicesResponse, error) { - out := new(ServicesResponse) - err := c.cc.Invoke(ctx, "/go.micro.network.Network/Services", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// NetworkServer is the server API for Network service. -type NetworkServer interface { - // Returns the entire network graph - Graph(context.Context, *GraphRequest) (*GraphResponse, error) - // Returns a list of known nodes in the network - Nodes(context.Context, *NodesRequest) (*NodesResponse, error) - // Returns a list of known routes in the network - Routes(context.Context, *RoutesRequest) (*RoutesResponse, error) - // Returns a list of known services based on routes - Services(context.Context, *ServicesRequest) (*ServicesResponse, error) -} - -func RegisterNetworkServer(s *grpc.Server, srv NetworkServer) { - s.RegisterService(&_Network_serviceDesc, srv) -} - -func _Network_Graph_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GraphRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(NetworkServer).Graph(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.network.Network/Graph", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NetworkServer).Graph(ctx, req.(*GraphRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Network_Nodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(NodesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(NetworkServer).Nodes(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.network.Network/Nodes", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NetworkServer).Nodes(ctx, req.(*NodesRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Network_Routes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RoutesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(NetworkServer).Routes(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.network.Network/Routes", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NetworkServer).Routes(ctx, req.(*RoutesRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Network_Services_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ServicesRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(NetworkServer).Services(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.network.Network/Services", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(NetworkServer).Services(ctx, req.(*ServicesRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _Network_serviceDesc = grpc.ServiceDesc{ - ServiceName: "go.micro.network.Network", - HandlerType: (*NetworkServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Graph", - Handler: _Network_Graph_Handler, - }, - { - MethodName: "Nodes", - Handler: _Network_Nodes_Handler, - }, - { - MethodName: "Routes", - Handler: _Network_Routes_Handler, - }, - { - MethodName: "Services", - Handler: _Network_Services_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "github.com/micro/go-micro/network/proto/network.proto", + // 576 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x54, 0x61, 0x6a, 0xdb, 0x4c, + 0x10, 0x8d, 0x2c, 0xcb, 0x76, 0xe6, 0x8b, 0xfd, 0xb9, 0x4b, 0x49, 0x85, 0x7e, 0xb4, 0xee, 0xe2, + 0x1f, 0xa1, 0x34, 0x32, 0x24, 0x04, 0x4a, 0x4d, 0x43, 0x20, 0x94, 0x42, 0x21, 0x21, 0x55, 0x2e, + 0x50, 0xc5, 0x1a, 0x6c, 0x93, 0x58, 0xeb, 0xac, 0xd6, 0x09, 0x3e, 0x41, 0x8f, 0xd0, 0x33, 0xf5, + 0x56, 0x65, 0x77, 0x47, 0x8a, 0x1d, 0xcb, 0xa2, 0xf9, 0xe7, 0xd1, 0xbc, 0xf7, 0x66, 0x67, 0xe6, + 0x8d, 0xe1, 0x64, 0x3c, 0x55, 0x93, 0xc5, 0x4d, 0x38, 0x12, 0xb3, 0xc1, 0x6c, 0x3a, 0x92, 0x62, + 0x30, 0x16, 0x87, 0xf6, 0x47, 0x8a, 0xea, 0x51, 0xc8, 0xdb, 0xc1, 0x5c, 0x0a, 0x55, 0x44, 0xa1, + 0x89, 0x58, 0x77, 0x2c, 0x42, 0x83, 0x0a, 0xe9, 0x7b, 0x30, 0xdc, 0x2e, 0x24, 0xc5, 0x42, 0xa1, + 0x1c, 0x64, 0x28, 0x1f, 0xa6, 0x23, 0x24, 0x3d, 0xfb, 0xd1, 0xca, 0xf1, 0x5f, 0x0e, 0x78, 0x3f, + 0x16, 0x28, 0x97, 0xcc, 0x87, 0x26, 0xe1, 0x7c, 0xa7, 0xe7, 0x1c, 0xec, 0x46, 0x79, 0xa8, 0x33, + 0x71, 0x92, 0x48, 0xcc, 0x32, 0xbf, 0x66, 0x33, 0x14, 0xea, 0xcc, 0x38, 0x56, 0xf8, 0x18, 0x2f, + 0x7d, 0xd7, 0x66, 0x28, 0x64, 0xfb, 0xd0, 0xb0, 0x75, 0xfc, 0xba, 0x49, 0x50, 0xa4, 0x19, 0xf4, + 0x6e, 0xdf, 0xb3, 0x0c, 0x0a, 0xf9, 0x29, 0x74, 0xce, 0x45, 0x9a, 0xe2, 0x48, 0x45, 0x78, 0xbf, + 0xc0, 0x4c, 0xb1, 0x8f, 0xe0, 0xa5, 0x22, 0xc1, 0xcc, 0x77, 0x7a, 0xee, 0xc1, 0x7f, 0x47, 0xfb, + 0xe1, 0xf3, 0xd6, 0xc3, 0x4b, 0x91, 0x60, 0x64, 0x41, 0xfc, 0x15, 0xfc, 0x5f, 0xf0, 0xb3, 0xb9, + 0x48, 0x33, 0xe4, 0x7d, 0xd8, 0xd3, 0x88, 0x2c, 0x17, 0x7c, 0x0d, 0x5e, 0x82, 0x73, 0x35, 0x31, + 0x0d, 0xb6, 0x23, 0x1b, 0xf0, 0x2f, 0xd0, 0x26, 0x94, 0xa5, 0xbd, 0xb0, 0x6e, 0x1f, 0xf6, 0xbe, + 0xc9, 0x78, 0x3e, 0xa9, 0x2e, 0x32, 0x84, 0x36, 0xa1, 0xa8, 0xc8, 0x07, 0xa8, 0x4b, 0x21, 0x94, + 0x41, 0x95, 0xd6, 0xb8, 0x42, 0x94, 0x91, 0xc1, 0xf0, 0x53, 0x68, 0x47, 0x7a, 0x7c, 0x45, 0x23, + 0x87, 0xe0, 0xdd, 0xeb, 0xa5, 0x11, 0xfb, 0xcd, 0x26, 0xdb, 0xec, 0x34, 0xb2, 0x28, 0x7e, 0x06, + 0x9d, 0x9c, 0x4f, 0xd5, 0x43, 0x5a, 0x4f, 0x49, 0x8f, 0x64, 0x0f, 0x43, 0xa0, 0xb5, 0x99, 0xe1, + 0x5e, 0x5b, 0x37, 0xe4, 0x6f, 0xe0, 0x21, 0x74, 0x9f, 0x3e, 0x91, 0x6c, 0x00, 0x2d, 0x32, 0x8d, + 0x15, 0xde, 0x8d, 0x8a, 0x98, 0xff, 0x71, 0xa0, 0xae, 0xe7, 0xc6, 0x3a, 0x50, 0x9b, 0x26, 0xe4, + 0xb1, 0xda, 0x34, 0xa9, 0xb6, 0x57, 0x6e, 0x16, 0x77, 0xcd, 0x2c, 0xec, 0x0c, 0x5a, 0x33, 0x54, + 0x71, 0x12, 0xab, 0xd8, 0xaf, 0x9b, 0x0e, 0xfa, 0xe5, 0x5b, 0x0a, 0x2f, 0x08, 0xf6, 0x35, 0x55, + 0x72, 0x19, 0x15, 0xac, 0x60, 0x08, 0xed, 0xb5, 0x14, 0xeb, 0x82, 0x7b, 0x8b, 0x4b, 0x7a, 0x97, + 0xfe, 0xa9, 0x37, 0xf9, 0x10, 0xdf, 0x2d, 0x90, 0x9e, 0x65, 0x83, 0xcf, 0xb5, 0x4f, 0x0e, 0x3f, + 0x81, 0x26, 0x79, 0x4d, 0xef, 0x51, 0xfb, 0x60, 0xfb, 0x1e, 0x8d, 0x57, 0x0c, 0x86, 0x1f, 0x83, + 0x77, 0x7e, 0x27, 0xec, 0xf2, 0xff, 0x99, 0xf4, 0x13, 0xea, 0xda, 0x0a, 0x2f, 0xe1, 0x68, 0x07, + 0xcf, 0x11, 0xa5, 0x1e, 0xa8, 0x5b, 0xe1, 0x2e, 0x0b, 0x3a, 0xfa, 0xed, 0x42, 0xf3, 0x92, 0x06, + 0x7b, 0xf5, 0xd4, 0x59, 0x6f, 0x93, 0xb5, 0x7e, 0xa0, 0xc1, 0xfb, 0x0a, 0x04, 0x9d, 0xe0, 0x0e, + 0xfb, 0x0e, 0x9e, 0x71, 0x3e, 0x7b, 0xbb, 0x89, 0x5e, 0x3d, 0x9c, 0xe0, 0xdd, 0xd6, 0xfc, 0xaa, + 0x96, 0x39, 0xd5, 0x32, 0xad, 0xd5, 0x4b, 0x2f, 0xd3, 0x5a, 0xbb, 0x71, 0xbe, 0xc3, 0x2e, 0xa0, + 0x61, 0x8f, 0x82, 0x95, 0x80, 0xd7, 0xce, 0x2d, 0xe8, 0x6d, 0x07, 0x14, 0x72, 0xd7, 0xd0, 0xca, + 0xcf, 0x81, 0x95, 0xcc, 0xe5, 0xd9, 0xf5, 0x04, 0xbc, 0x0a, 0x92, 0x8b, 0xde, 0x34, 0xcc, 0x9f, + 0xf4, 0xf1, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa7, 0x5b, 0x0a, 0x25, 0x2c, 0x06, 0x00, 0x00, } diff --git a/network/proto/network.micro.go b/network/service/proto/network.pb.micro.go similarity index 85% rename from network/proto/network.micro.go rename to network/service/proto/network.pb.micro.go index 7dee3697..488d0c97 100644 --- a/network/proto/network.micro.go +++ b/network/service/proto/network.pb.micro.go @@ -6,7 +6,7 @@ package go_micro_network import ( fmt "fmt" proto "github.com/golang/protobuf/proto" - _ "github.com/micro/go-micro/router/proto" + _ "github.com/micro/go-micro/router/service/proto" math "math" ) @@ -35,6 +35,8 @@ var _ server.Option // Client API for Network service type NetworkService interface { + // Connect to the network + Connect(ctx context.Context, in *ConnectRequest, opts ...client.CallOption) (*ConnectResponse, error) // Returns the entire network graph Graph(ctx context.Context, in *GraphRequest, opts ...client.CallOption) (*GraphResponse, error) // Returns a list of known nodes in the network @@ -63,6 +65,16 @@ func NewNetworkService(name string, c client.Client) NetworkService { } } +func (c *networkService) Connect(ctx context.Context, in *ConnectRequest, opts ...client.CallOption) (*ConnectResponse, error) { + req := c.c.NewRequest(c.name, "Network.Connect", in) + out := new(ConnectResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *networkService) Graph(ctx context.Context, in *GraphRequest, opts ...client.CallOption) (*GraphResponse, error) { req := c.c.NewRequest(c.name, "Network.Graph", in) out := new(GraphResponse) @@ -106,6 +118,8 @@ func (c *networkService) Services(ctx context.Context, in *ServicesRequest, opts // Server API for Network service type NetworkHandler interface { + // Connect to the network + Connect(context.Context, *ConnectRequest, *ConnectResponse) error // Returns the entire network graph Graph(context.Context, *GraphRequest, *GraphResponse) error // Returns a list of known nodes in the network @@ -118,6 +132,7 @@ type NetworkHandler interface { func RegisterNetworkHandler(s server.Server, hdlr NetworkHandler, opts ...server.HandlerOption) error { type network interface { + Connect(ctx context.Context, in *ConnectRequest, out *ConnectResponse) error Graph(ctx context.Context, in *GraphRequest, out *GraphResponse) error Nodes(ctx context.Context, in *NodesRequest, out *NodesResponse) error Routes(ctx context.Context, in *RoutesRequest, out *RoutesResponse) error @@ -134,6 +149,10 @@ type networkHandler struct { NetworkHandler } +func (h *networkHandler) Connect(ctx context.Context, in *ConnectRequest, out *ConnectResponse) error { + return h.NetworkHandler.Connect(ctx, in, out) +} + func (h *networkHandler) Graph(ctx context.Context, in *GraphRequest, out *GraphResponse) error { return h.NetworkHandler.Graph(ctx, in, out) } diff --git a/network/proto/network.proto b/network/service/proto/network.proto similarity index 74% rename from network/proto/network.proto rename to network/service/proto/network.proto index a82d75a1..b4dece64 100644 --- a/network/proto/network.proto +++ b/network/service/proto/network.proto @@ -2,10 +2,12 @@ syntax = "proto3"; package go.micro.network; -import "github.com/micro/go-micro/router/proto/router.proto"; +import "github.com/micro/go-micro/router/service/proto/router.proto"; // Network service is usesd to gain visibility into networks service Network { + // Connect to the network + rpc Connect(ConnectRequest) returns (ConnectResponse) {}; // Returns the entire network graph rpc Graph(GraphRequest) returns (GraphResponse) {}; // Returns a list of known nodes in the network @@ -16,6 +18,21 @@ service Network { rpc Services(ServicesRequest) returns (ServicesResponse) {}; } +// Query is passed in a LookupRequest +message Query { + string service = 1; + string address = 2; + string gateway = 3; + string router = 4; + string network = 5; +} + +message ConnectRequest { + repeated Node nodes = 1; +} + +message ConnectResponse {} + // PeerRequest requests list of peers message NodesRequest { // node topology depth @@ -38,6 +55,8 @@ message GraphResponse { } message RoutesRequest { + // filter based on + Query query = 1; } message RoutesResponse { @@ -56,6 +75,10 @@ message Node { string id = 1; // node address string address = 2; + // the network + string network = 3; + // associated metadata + map metadata = 4; } // Connect is sent when the node connects to the network diff --git a/options.go b/options.go index d566a353..aa90c88f 100644 --- a/options.go +++ b/options.go @@ -31,6 +31,8 @@ type Options struct { // Other options for implementations of the interface // can be stored in a context Context context.Context + + Signal bool } func newOptions(opts ...Option) Options { @@ -42,6 +44,7 @@ func newOptions(opts ...Option) Options { Registry: registry.DefaultRegistry, Transport: transport.DefaultTransport, Context: context.Background(), + Signal: true, } for _, o := range opts { @@ -81,6 +84,15 @@ func Context(ctx context.Context) Option { } } +// HandleSignal toggles automatic installation of the signal handler that +// traps TERM, INT, and QUIT. Users of this feature to disable the signal +// handler, should control liveness of the service through the context. +func HandleSignal(b bool) Option { + return func(o *Options) { + o.Signal = b + } +} + func Server(s server.Server) Option { return func(o *Options) { o.Server = s diff --git a/plugin/default.go b/plugin/default.go index bcafbc3b..8fde651a 100644 --- a/plugin/default.go +++ b/plugin/default.go @@ -121,5 +121,7 @@ func (p *plugin) Build(path string, c *Config) error { return fmt.Errorf("Failed to create dir %s: %v", filepath.Dir(path), err) } cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", path+".so", goFile) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr return cmd.Run() } diff --git a/proxy/grpc/grpc.go b/proxy/grpc/grpc.go index 89947054..571822ea 100644 --- a/proxy/grpc/grpc.go +++ b/proxy/grpc/grpc.go @@ -10,7 +10,6 @@ import ( "github.com/micro/go-micro/client/grpc" "github.com/micro/go-micro/codec" "github.com/micro/go-micro/config/options" - "github.com/micro/go-micro/errors" "github.com/micro/go-micro/proxy" "github.com/micro/go-micro/server" ) @@ -62,8 +61,14 @@ func readLoop(r server.Request, s client.Stream) error { } } -func (p *Proxy) SendRequest(ctx context.Context, req client.Request, rsp client.Response) error { - return errors.InternalServerError("go.micro.proxy.grpc", "SendRequest is unsupported") +// ProcessMessage acts as a message exchange and forwards messages to ongoing topics +// TODO: should we look at p.Endpoint and only send to the local endpoint? probably +func (p *Proxy) ProcessMessage(ctx context.Context, msg server.Message) error { + // TODO: check that we're not broadcast storming by sending to the same topic + // that we're actually subscribed to + + // directly publish to the local client + return p.Client.Publish(ctx, msg) } // ServeRequest honours the server.Proxy interface @@ -130,8 +135,6 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server return err } } - - return nil } // NewProxy returns a new grpc proxy server diff --git a/proxy/http/http.go b/proxy/http/http.go index 37ef1f87..64b4384b 100644 --- a/proxy/http/http.go +++ b/proxy/http/http.go @@ -10,7 +10,6 @@ import ( "net/url" "path" - "github.com/micro/go-micro/client" "github.com/micro/go-micro/config/options" "github.com/micro/go-micro/errors" "github.com/micro/go-micro/proxy" @@ -45,8 +44,69 @@ func getEndpoint(hdr map[string]string) string { return "" } -func (p *Proxy) SendRequest(ctx context.Context, req client.Request, rsp client.Response) error { - return errors.InternalServerError("go.micro.proxy.http", "SendRequest is unsupported") +func getTopic(hdr map[string]string) string { + ep := hdr["Micro-Topic"] + if len(ep) > 0 && ep[0] == '/' { + return ep + } + return "/" + hdr["Micro-Topic"] +} + +// ProcessMessage handles incoming asynchronous messages +func (p *Proxy) ProcessMessage(ctx context.Context, msg server.Message) error { + if p.Endpoint == "" { + p.Endpoint = proxy.DefaultEndpoint + } + + // get the header + hdr := msg.Header() + + // get topic + // use /topic as endpoint + endpoint := getTopic(hdr) + + // set the endpoint + if len(endpoint) == 0 { + endpoint = p.Endpoint + } else { + // add endpoint to backend + u, err := url.Parse(p.Endpoint) + if err != nil { + return errors.InternalServerError(msg.Topic(), err.Error()) + } + u.Path = path.Join(u.Path, endpoint) + endpoint = u.String() + } + + // send to backend + hreq, err := http.NewRequest("POST", endpoint, bytes.NewReader(msg.Body())) + if err != nil { + return errors.InternalServerError(msg.Topic(), err.Error()) + } + + // set the headers + for k, v := range hdr { + hreq.Header.Set(k, v) + } + + // make the call + hrsp, err := http.DefaultClient.Do(hreq) + if err != nil { + return errors.InternalServerError(msg.Topic(), err.Error()) + } + + // read body + b, err := ioutil.ReadAll(hrsp.Body) + hrsp.Body.Close() + if err != nil { + return errors.InternalServerError(msg.Topic(), err.Error()) + } + + if hrsp.StatusCode != 200 { + return errors.New(msg.Topic(), string(b), int32(hrsp.StatusCode)) + } + + return nil } // ServeRequest honours the server.Router interface @@ -113,7 +173,7 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server // set response headers hdr = map[string]string{} - for k, _ := range hrsp.Header { + for k := range hrsp.Header { hdr[k] = hrsp.Header.Get(k) } // write the header @@ -127,8 +187,6 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server return errors.InternalServerError(req.Service(), err.Error()) } } - - return nil } // NewSingleHostProxy returns a router which sends requests to a single http backend diff --git a/proxy/mucp/mucp.go b/proxy/mucp/mucp.go index 8cd6df85..d565d47a 100644 --- a/proxy/mucp/mucp.go +++ b/proxy/mucp/mucp.go @@ -11,13 +11,16 @@ import ( "time" "github.com/micro/go-micro/client" + "github.com/micro/go-micro/client/selector" "github.com/micro/go-micro/codec" "github.com/micro/go-micro/codec/bytes" "github.com/micro/go-micro/config/options" "github.com/micro/go-micro/errors" + "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/proxy" "github.com/micro/go-micro/router" "github.com/micro/go-micro/server" + "github.com/micro/go-micro/util/log" ) // Proxy will transparently proxy requests to an endpoint. @@ -41,9 +44,6 @@ type Proxy struct { // A fib of routes service:address sync.RWMutex Routes map[string]map[uint64]router.Route - - // The channel to monitor watcher errors - errChan chan error } // read client request and write to server @@ -83,7 +83,8 @@ func readLoop(r server.Request, s client.Stream) error { // toNodes returns a list of node addresses from given routes func toNodes(routes []router.Route) []string { - var nodes []string + nodes := make([]string, 0, len(routes)) + for _, node := range routes { address := node.Address if len(node.Gateway) > 0 { @@ -91,9 +92,83 @@ func toNodes(routes []router.Route) []string { } nodes = append(nodes, address) } + return nodes } +func toSlice(r map[uint64]router.Route) []router.Route { + routes := make([]router.Route, 0, len(r)) + + for _, v := range r { + routes = append(routes, v) + } + + // sort the routes in order of metric + sort.Slice(routes, func(i, j int) bool { return routes[i].Metric < routes[j].Metric }) + + return routes +} + +func (p *Proxy) filterRoutes(ctx context.Context, routes []router.Route) []router.Route { + md, ok := metadata.FromContext(ctx) + if !ok { + return routes + } + + //nolint:prealloc + var filteredRoutes []router.Route + + // filter the routes based on our headers + for _, route := range routes { + // process only routes for this id + if id := md["Micro-Router"]; len(id) > 0 { + if route.Router != id { + // skip routes that don't mwatch + continue + } + } + + // only process routes with this network + if net := md["Micro-Network"]; len(net) > 0 { + if route.Network != net { + // skip routes that don't mwatch + continue + } + } + + // process only this gateway + if gw := md["Micro-Gateway"]; len(gw) > 0 { + // if the gateway matches our address + // special case, take the routes with no gateway + // TODO: should we strip the gateway from the context? + if gw == p.Router.Options().Address { + if len(route.Gateway) > 0 && route.Gateway != gw { + continue + } + // otherwise its a local route and we're keeping it + } else { + // gateway does not match our own + if route.Gateway != gw { + continue + } + } + } + + // TODO: address based filtering + // address := md["Micro-Address"] + + // TODO: label based filtering + // requires new field in routing table : route.Labels + + // passed the filter checks + filteredRoutes = append(filteredRoutes, route) + } + + log.Tracef("Proxy filtered routes %+v\n", filteredRoutes) + + return filteredRoutes +} + func (p *Proxy) getLink(r router.Route) (client.Client, error) { if r.Link == "local" || len(p.Links) == 0 { return p.Client, nil @@ -105,30 +180,29 @@ func (p *Proxy) getLink(r router.Route) (client.Client, error) { return l, nil } -func (p *Proxy) getRoute(service string) ([]router.Route, error) { - toSlice := func(r map[uint64]router.Route) []router.Route { - var routes []router.Route - for _, v := range r { - routes = append(routes, v) - } - - // sort the routes in order of metric - sort.Slice(routes, func(i, j int) bool { return routes[i].Metric < routes[j].Metric }) - - return routes - } - +func (p *Proxy) getRoute(ctx context.Context, service string) ([]router.Route, error) { // lookup the route cache first p.Lock() - routes, ok := p.Routes[service] + cached, ok := p.Routes[service] if ok { p.Unlock() - return toSlice(routes), nil + routes := toSlice(cached) + return p.filterRoutes(ctx, routes), nil } p.Unlock() + // cache routes for the service + routes, err := p.cacheRoutes(service) + if err != nil { + return nil, err + } + + return p.filterRoutes(ctx, routes), nil +} + +func (p *Proxy) cacheRoutes(service string) ([]router.Route, error) { // lookup the routes in the router - results, err := p.Router.Lookup(router.NewQuery(router.QueryService(service))) + results, err := p.Router.Lookup(router.QueryService(service)) if err != nil { // check the status of the router if status := p.Router.Status(); status.Code == router.Error { @@ -147,22 +221,53 @@ func (p *Proxy) getRoute(service string) ([]router.Route, error) { } p.Routes[service][route.Hash()] = route } - routes = p.Routes[service] + routes := p.Routes[service] p.Unlock() return toSlice(routes), nil } -// manageRouteCache applies action on a given route to Proxy route cache -func (p *Proxy) manageRouteCache(route router.Route, action string) error { +// refreshMetrics will refresh any metrics for our local cached routes. +// we may not receive new watch events for these as they change. +func (p *Proxy) refreshMetrics() { + // get a list of services to update + p.RLock() + + services := make([]string, 0, len(p.Routes)) + + for service := range p.Routes { + services = append(services, service) + } + + p.RUnlock() + + // get and cache the routes for the service + for _, service := range services { + p.cacheRoutes(service) + } +} + +// manageRoutes applies action on a given route to Proxy route cache +func (p *Proxy) manageRoutes(route router.Route, action string) error { + // we only cache what we are actually concerned with + p.Lock() + defer p.Unlock() + + log.Tracef("Proxy taking route action %v %+v\n", action, route) + switch action { case "create", "update": if _, ok := p.Routes[route.Service]; !ok { - p.Routes[route.Service] = make(map[uint64]router.Route) + return fmt.Errorf("not called %s", route.Service) } p.Routes[route.Service][route.Hash()] = route case "delete": + // delete that specific route delete(p.Routes[route.Service], route.Hash()) + // clean up the cache entirely + if len(p.Routes[route.Service]) == 0 { + delete(p.Routes, route.Service) + } default: return fmt.Errorf("unknown action: %s", action) } @@ -172,36 +277,53 @@ func (p *Proxy) manageRouteCache(route router.Route, action string) error { // watchRoutes watches service routes and updates proxy cache func (p *Proxy) watchRoutes() { - // this is safe to do as the only way watchRoutes returns is - // when some error is written into error channel - we want to bail then - defer close(p.errChan) - // route watcher w, err := p.Router.Watch() if err != nil { - p.errChan <- err return } for { event, err := w.Next() if err != nil { - p.errChan <- err return } - p.Lock() - if err := p.manageRouteCache(event.Route, fmt.Sprintf("%s", event.Type)); err != nil { + if err := p.manageRoutes(event.Route, event.Type.String()); err != nil { // TODO: should we bail here? - p.Unlock() continue } - p.Unlock() } } -func (p *Proxy) SendRequest(ctx context.Context, req client.Request, rsp client.Response) error { - return errors.InternalServerError("go.micro.proxy", "SendRequest is unsupported") +// ProcessMessage acts as a message exchange and forwards messages to ongoing topics +// TODO: should we look at p.Endpoint and only send to the local endpoint? probably +func (p *Proxy) ProcessMessage(ctx context.Context, msg server.Message) error { + // TODO: check that we're not broadcast storming by sending to the same topic + // that we're actually subscribed to + + log.Tracef("Proxy received message for %s", msg.Topic()) + + var errors []string + + // directly publish to the local client + if err := p.Client.Publish(ctx, msg); err != nil { + errors = append(errors, err.Error()) + } + + // publish to all links + for _, client := range p.Links { + if err := client.Publish(ctx, msg); err != nil { + errors = append(errors, err.Error()) + } + } + + if len(errors) == 0 { + return nil + } + + // there is no error...muahaha + return fmt.Errorf("Message processing error: %s", strings.Join(errors, "\n")) } // ServeRequest honours the server.Router interface @@ -221,6 +343,8 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server return errors.BadRequest("go.micro.proxy", "service name is blank") } + log.Tracef("Proxy received request for %s", service) + // are we network routing or local routing if len(p.Links) == 0 { local = true @@ -233,7 +357,7 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server addresses = []string{p.Endpoint} } else { // get route for endpoint from router - addr, err := p.getRoute(p.Endpoint) + addr, err := p.getRoute(ctx, p.Endpoint) if err != nil { return err } @@ -245,18 +369,28 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server } else { // no endpoint was specified just lookup the route // get route for endpoint from router - addr, err := p.getRoute(service) + addr, err := p.getRoute(ctx, service) if err != nil { return err } routes = addr } + //nolint:prealloc + opts := []client.CallOption{ + // set strategy to round robin + client.WithSelectOption(selector.WithStrategy(selector.RoundRobin)), + } + // if the address is already set just serve it // TODO: figure it out if we should know to pick a link if len(addresses) > 0 { + opts = append(opts, + client.WithAddress(addresses...), + ) + // serve the normal way - return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp, client.WithAddress(addresses...)) + return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp, opts...) } // there's no links e.g we're local routing then just serve it with addresses @@ -269,10 +403,16 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server opts = append(opts, client.WithAddress(addresses...)) } + log.Tracef("Proxy calling %+v\n", addresses) // serve the normal way return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp, opts...) } + // we're assuming we need routes to operate on + if len(routes) == 0 { + return errors.InternalServerError("go.micro.proxy", "route not found") + } + var gerr error // we're routing globally with multiple links @@ -286,11 +426,19 @@ func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server continue } + log.Tracef("Proxy using route %+v\n", route) + // set the address to call addresses := toNodes([]router.Route{route}) + // set the address in the options + // disable retries since its one route processing + opts = append(opts, + client.WithAddress(addresses...), + client.WithRetries(0), + ) // do the request with the link - gerr = p.serveRequest(ctx, link, service, endpoint, req, rsp, client.WithAddress(addresses...)) + gerr = p.serveRequest(ctx, link, service, endpoint, req, rsp, opts...) // return on no error since we succeeded if gerr == nil { return nil @@ -316,7 +464,7 @@ func (p *Proxy) serveRequest(ctx context.Context, link client.Client, service, e } // create new request with raw bytes body - creq := link.NewRequest(service, endpoint, &bytes.Frame{body}, client.WithContentType(req.ContentType())) + creq := link.NewRequest(service, endpoint, &bytes.Frame{Data: body}, client.WithContentType(req.ContentType())) // not a stream so make a client.Call request if !req.Stream() { @@ -348,43 +496,30 @@ func (p *Proxy) serveRequest(ctx context.Context, link client.Client, service, e // get raw response resp := stream.Response() - // route watcher error - var watchErr error - // create server response write loop for { - select { - case err := <-p.errChan: - if err != nil { - watchErr = err - } - return watchErr - default: - // read backend response body - body, err := resp.Read() - if err == io.EOF { - return nil - } else if err != nil { - return err - } + // read backend response body + body, err := resp.Read() + if err == io.EOF { + return nil + } else if err != nil { + return err + } - // read backend response header - hdr := resp.Header() + // read backend response header + hdr := resp.Header() - // write raw response header to client - rsp.WriteHeader(hdr) + // write raw response header to client + rsp.WriteHeader(hdr) - // write raw response body to client - err = rsp.Write(body) - if err == io.EOF { - return nil - } else if err != nil { - return err - } + // write raw response body to client + err = rsp.Write(body) + if err == io.EOF { + return nil + } else if err != nil { + return err } } - - return nil } // NewSingleHostProxy returns a proxy which sends requests to a single backend @@ -439,9 +574,6 @@ func NewProxy(opts ...options.Option) proxy.Proxy { // routes cache p.Routes = make(map[string]map[uint64]router.Route) - // watch router service routes - p.errChan = make(chan error, 1) - go func() { // continuously attempt to watch routes for { @@ -452,5 +584,18 @@ func NewProxy(opts ...options.Option) proxy.Proxy { } }() + go func() { + // TODO: speed up refreshing of metrics + // without this ticking effort e.g stream + t := time.NewTicker(time.Second * 10) + defer t.Stop() + + // we must refresh route metrics since they do not trigger new events + for range t.C { + // refresh route metrics + p.refreshMetrics() + } + }() + return p } diff --git a/proxy/proxy.go b/proxy/proxy.go index e91eaf0f..e08926e9 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -13,9 +13,9 @@ import ( // Proxy can be used as a proxy server for go-micro services type Proxy interface { options.Options - // SendRequest honours the client.Router interface - SendRequest(context.Context, client.Request, client.Response) error - // ServeRequest honours the server.Router interface + // ProcessMessage handles inbound messages + ProcessMessage(context.Context, server.Message) error + // ServeRequest handles inbound requests ServeRequest(context.Context, server.Request, server.Response) error } diff --git a/registry/cache/rcache.go b/registry/cache/cache.go similarity index 89% rename from registry/cache/rcache.go rename to registry/cache/cache.go index 64ec6843..75fcac32 100644 --- a/registry/cache/rcache.go +++ b/registry/cache/cache.go @@ -39,6 +39,8 @@ type cache struct { // used to stop the cache exit chan bool + // indicate whether its running + running bool // status of the registry // used to hold onto the cache // in failure state @@ -81,7 +83,7 @@ func (c *cache) isValid(services []*registry.Service, ttl time.Time) bool { } // time since ttl is longer than timeout - if time.Since(ttl) > c.opts.TTL { + if time.Since(ttl) > 0 { return false } @@ -100,7 +102,7 @@ func (c *cache) quit() bool { func (c *cache) del(service string) { // don't blow away cache in error state - if err := c.getStatus(); err != nil { + if err := c.status; err != nil { return } // otherwise delete entries @@ -116,14 +118,13 @@ func (c *cache) get(service string) ([]*registry.Service, error) { services := c.cache[service] // get cache ttl ttl := c.ttls[service] + // make a copy + cp := registry.Copy(services) // got services && within ttl so return cache - if c.isValid(services, ttl) { - // make a copy - cp := registry.Copy(services) - // unlock the read + if c.isValid(cp, ttl) { c.RUnlock() - // return servics + // return services return cp, nil } @@ -136,15 +137,16 @@ func (c *cache) get(service string) ([]*registry.Service, error) { if len(cached) > 0 { // set the error status c.setStatus(err) + // return the stale cache - return registry.Copy(cached), nil + return cached, nil } // otherwise return error return nil, err } // reset the status - if c.getStatus(); err != nil { + if err := c.getStatus(); err != nil { c.setStatus(nil) } @@ -157,15 +159,28 @@ func (c *cache) get(service string) ([]*registry.Service, error) { } // watch service if not watched - if _, ok := c.watched[service]; !ok { - go c.run(service) - } + _, ok := c.watched[service] // unlock the read lock c.RUnlock() + // check if its being watched + if !ok { + c.Lock() + + // set to watched + c.watched[service] = true + + // only kick it off if not running + if !c.running { + go c.run() + } + + c.Unlock() + } + // get and return services - return get(service, services) + return get(service, cp) } func (c *cache) set(service string, services []*registry.Service) { @@ -181,6 +196,11 @@ func (c *cache) update(res *registry.Result) { c.Lock() defer c.Unlock() + // only save watched services + if _, ok := c.watched[res.Service.Name]; !ok { + return + } + services, ok := c.cache[res.Service.Name] if !ok { // we're not going to cache anything @@ -283,16 +303,16 @@ func (c *cache) update(res *registry.Result) { // run starts the cache watcher loop // it creates a new watcher if there's a problem -func (c *cache) run(service string) { - // set watcher +func (c *cache) run() { c.Lock() - c.watched[service] = true + c.running = true c.Unlock() - // delete watcher on exit + // reset watcher on exit defer func() { c.Lock() - delete(c.watched, service) + c.watched = make(map[string]bool) + c.running = false c.Unlock() }() @@ -309,10 +329,7 @@ func (c *cache) run(service string) { time.Sleep(time.Duration(j) * time.Millisecond) // create new watcher - w, err := c.Registry.Watch( - registry.WatchService(service), - ) - + w, err := c.Registry.Watch() if err != nil { if c.quit() { return @@ -414,6 +431,9 @@ func (c *cache) GetService(service string) ([]*registry.Service, error) { } func (c *cache) Stop() { + c.Lock() + defer c.Unlock() + select { case <-c.exit: return @@ -423,7 +443,7 @@ func (c *cache) Stop() { } func (c *cache) String() string { - return "rcache" + return "cache" } // New returns a new cache diff --git a/registry/consul/consul.go b/registry/consul/consul.go deleted file mode 100644 index 007d7369..00000000 --- a/registry/consul/consul.go +++ /dev/null @@ -1,438 +0,0 @@ -package consul - -import ( - "crypto/tls" - "errors" - "fmt" - "net" - "net/http" - "runtime" - "strconv" - "sync" - "time" - - consul "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/registry" - mnet "github.com/micro/go-micro/util/net" - hash "github.com/mitchellh/hashstructure" -) - -type consulRegistry struct { - Address []string - opts registry.Options - - client *consul.Client - config *consul.Config - - // 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 - var addrs []string - - // iterate the options addresses - for _, address := range c.opts.Addrs { - // check we have a port - addr, port, err := net.SplitHostPort(address) - if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { - port = "8500" - addr = address - addrs = append(addrs, fmt.Sprintf("%s:%s", addr, port)) - } else if err == nil { - addrs = append(addrs, fmt.Sprintf("%s:%s", addr, port)) - } - } - - // set the addrs - if len(addrs) > 0 { - c.Address = addrs - config.Address = c.Address[0] - } - - if config.HttpClient == nil { - config.HttpClient = new(http.Client) - } - - // requires secure connection? - if c.opts.Secure || c.opts.TLSConfig != nil { - 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 - } - - // set the config - c.config = config - - // remove client - c.client = nil - - // setup the client - c.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: node.Address, - 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), - } - } - - host, pt, _ := net.SplitHostPort(node.Address) - if host == "" { - host = node.Address - } - port, _ := strconv.Atoi(pt) - - // register the service - asr := &consul.AgentServiceRegistration{ - ID: node.Id, - Name: s.Name, - Tags: tags, - Port: port, - Address: host, - 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 = ®istry.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, ®istry.Node{ - Id: id, - Address: mnet.HostPort(address, 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, ®istry.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 (c *consulRegistry) Client() *consul.Client { - if c.client != nil { - return c.client - } - - for _, addr := range c.Address { - // set the address - c.config.Address = addr - - // create a new client - tmpClient, _ := consul.NewClient(c.config) - - // test the client - _, err := tmpClient.Agent().Host() - if err != nil { - continue - } - - // set the client - c.client = tmpClient - return c.client - } - - // set the default - c.client, _ = consul.NewClient(c.config) - - // return the client - return c.client -} - -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 -} diff --git a/registry/consul/encoding.go b/registry/consul/encoding.go deleted file mode 100644 index 8a152683..00000000 --- a/registry/consul/encoding.go +++ /dev/null @@ -1,170 +0,0 @@ -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 -} diff --git a/registry/consul/encoding_test.go b/registry/consul/encoding_test.go deleted file mode 100644 index 7f511174..00000000 --- a/registry/consul/encoding_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package consul - -import ( - "encoding/json" - "testing" - - "github.com/micro/go-micro/registry" -) - -func TestEncodingEndpoints(t *testing.T) { - eps := []*registry.Endpoint{ - ®istry.Endpoint{ - Name: "endpoint1", - Request: ®istry.Value{ - Name: "request", - Type: "request", - }, - Response: ®istry.Value{ - Name: "response", - Type: "response", - }, - Metadata: map[string]string{ - "foo1": "bar1", - }, - }, - ®istry.Endpoint{ - Name: "endpoint2", - Request: ®istry.Value{ - Name: "request", - Type: "request", - }, - Response: ®istry.Value{ - Name: "response", - Type: "response", - }, - Metadata: map[string]string{ - "foo2": "bar2", - }, - }, - ®istry.Endpoint{ - Name: "endpoint3", - Request: ®istry.Value{ - Name: "request", - Type: "request", - }, - Response: ®istry.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) - } - } -} diff --git a/registry/consul/options.go b/registry/consul/options.go deleted file mode 100644 index 29bc3ee5..00000000 --- a/registry/consul/options.go +++ /dev/null @@ -1,81 +0,0 @@ -package consul - -import ( - "context" - "time" - - consul "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/registry" -) - -// Connect specifies services should be registered as Consul Connect services -func Connect() registry.Option { - return func(o *registry.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, "consul_connect", true) - } -} - -func Config(c *consul.Config) registry.Option { - return func(o *registry.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, "consul_config", c) - } -} - -// AllowStale sets whether any Consul server (non-leader) can service -// a read. This allows for lower latency and higher throughput -// at the cost of potentially stale data. -// Works similar to Consul DNS Config option [1]. -// Defaults to true. -// -// [1] https://www.consul.io/docs/agent/options.html#allow_stale -// -func AllowStale(v bool) registry.Option { - return func(o *registry.Options) { - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, "consul_allow_stale", v) - } -} - -// QueryOptions specifies the QueryOptions to be used when calling -// Consul. See `Consul API` for more information [1]. -// -// [1] https://godoc.org/github.com/hashicorp/consul/api#QueryOptions -// -func QueryOptions(q *consul.QueryOptions) registry.Option { - return func(o *registry.Options) { - if q == nil { - return - } - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, "consul_query_options", q) - } -} - -// -// TCPCheck will tell the service provider to check the service address -// and port every `t` interval. It will enabled only if `t` is greater than 0. -// See `TCP + Interval` for more information [1]. -// -// [1] https://www.consul.io/docs/agent/checks.html -// -func TCPCheck(t time.Duration) registry.Option { - return func(o *registry.Options) { - if t <= time.Duration(0) { - return - } - if o.Context == nil { - o.Context = context.Background() - } - o.Context = context.WithValue(o.Context, "consul_tcp_check", t) - } -} diff --git a/registry/consul/registry_test.go b/registry/consul/registry_test.go deleted file mode 100644 index 3a275030..00000000 --- a/registry/consul/registry_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package consul - -import ( - "bytes" - "encoding/json" - "errors" - "net" - "net/http" - "testing" - "time" - - consul "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/registry" -) - -type mockRegistry struct { - body []byte - status int - err error - url string -} - -func encodeData(obj interface{}) ([]byte, error) { - buf := bytes.NewBuffer(nil) - enc := json.NewEncoder(buf) - if err := enc.Encode(obj); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func newMockServer(rg *mockRegistry, l net.Listener) error { - mux := http.NewServeMux() - mux.HandleFunc(rg.url, func(w http.ResponseWriter, r *http.Request) { - if rg.err != nil { - http.Error(w, rg.err.Error(), 500) - return - } - w.WriteHeader(rg.status) - w.Write(rg.body) - }) - return http.Serve(l, mux) -} - -func newConsulTestRegistry(r *mockRegistry) (*consulRegistry, func()) { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - // blurgh?!! - panic(err.Error()) - } - cfg := consul.DefaultConfig() - cfg.Address = l.Addr().String() - - go newMockServer(r, l) - - var cr = &consulRegistry{ - config: cfg, - Address: []string{cfg.Address}, - opts: registry.Options{}, - register: make(map[string]uint64), - lastChecked: make(map[string]time.Time), - queryOptions: &consul.QueryOptions{ - AllowStale: true, - }, - } - cr.Client() - - return cr, func() { - l.Close() - } -} - -func newServiceList(svc []*consul.ServiceEntry) []byte { - bts, _ := encodeData(svc) - return bts -} - -func TestConsul_GetService_WithError(t *testing.T) { - cr, cl := newConsulTestRegistry(&mockRegistry{ - err: errors.New("client-error"), - url: "/v1/health/service/service-name", - }) - defer cl() - - if _, err := cr.GetService("test-service"); err == nil { - t.Fatalf("Expected error not to be `nil`") - } -} - -func TestConsul_GetService_WithHealthyServiceNodes(t *testing.T) { - // warning is still seen as healthy, critical is not - svcs := []*consul.ServiceEntry{ - newServiceEntry( - "node-name-1", "node-address-1", "service-name", "v1.0.0", - []*consul.HealthCheck{ - newHealthCheck("node-name-1", "service-name", "passing"), - newHealthCheck("node-name-1", "service-name", "warning"), - }, - ), - newServiceEntry( - "node-name-2", "node-address-2", "service-name", "v1.0.0", - []*consul.HealthCheck{ - newHealthCheck("node-name-2", "service-name", "passing"), - newHealthCheck("node-name-2", "service-name", "warning"), - }, - ), - } - - cr, cl := newConsulTestRegistry(&mockRegistry{ - status: 200, - body: newServiceList(svcs), - url: "/v1/health/service/service-name", - }) - defer cl() - - svc, err := cr.GetService("service-name") - if err != nil { - t.Fatal("Unexpected error", err) - } - - if exp, act := 1, len(svc); exp != act { - t.Fatalf("Expected len of svc to be `%d`, got `%d`.", exp, act) - } - - if exp, act := 2, len(svc[0].Nodes); exp != act { - t.Fatalf("Expected len of nodes to be `%d`, got `%d`.", exp, act) - } -} - -func TestConsul_GetService_WithUnhealthyServiceNode(t *testing.T) { - // warning is still seen as healthy, critical is not - svcs := []*consul.ServiceEntry{ - newServiceEntry( - "node-name-1", "node-address-1", "service-name", "v1.0.0", - []*consul.HealthCheck{ - newHealthCheck("node-name-1", "service-name", "passing"), - newHealthCheck("node-name-1", "service-name", "warning"), - }, - ), - newServiceEntry( - "node-name-2", "node-address-2", "service-name", "v1.0.0", - []*consul.HealthCheck{ - newHealthCheck("node-name-2", "service-name", "passing"), - newHealthCheck("node-name-2", "service-name", "critical"), - }, - ), - } - - cr, cl := newConsulTestRegistry(&mockRegistry{ - status: 200, - body: newServiceList(svcs), - url: "/v1/health/service/service-name", - }) - defer cl() - - svc, err := cr.GetService("service-name") - if err != nil { - t.Fatal("Unexpected error", err) - } - - if exp, act := 1, len(svc); exp != act { - t.Fatalf("Expected len of svc to be `%d`, got `%d`.", exp, act) - } - - if exp, act := 1, len(svc[0].Nodes); exp != act { - t.Fatalf("Expected len of nodes to be `%d`, got `%d`.", exp, act) - } -} - -func TestConsul_GetService_WithUnhealthyServiceNodes(t *testing.T) { - // warning is still seen as healthy, critical is not - svcs := []*consul.ServiceEntry{ - newServiceEntry( - "node-name-1", "node-address-1", "service-name", "v1.0.0", - []*consul.HealthCheck{ - newHealthCheck("node-name-1", "service-name", "passing"), - newHealthCheck("node-name-1", "service-name", "critical"), - }, - ), - newServiceEntry( - "node-name-2", "node-address-2", "service-name", "v1.0.0", - []*consul.HealthCheck{ - newHealthCheck("node-name-2", "service-name", "passing"), - newHealthCheck("node-name-2", "service-name", "critical"), - }, - ), - } - - cr, cl := newConsulTestRegistry(&mockRegistry{ - status: 200, - body: newServiceList(svcs), - url: "/v1/health/service/service-name", - }) - defer cl() - - svc, err := cr.GetService("service-name") - if err != nil { - t.Fatal("Unexpected error", err) - } - - if exp, act := 1, len(svc); exp != act { - t.Fatalf("Expected len of svc to be `%d`, got `%d`.", exp, act) - } - - if exp, act := 0, len(svc[0].Nodes); exp != act { - t.Fatalf("Expected len of nodes to be `%d`, got `%d`.", exp, act) - } -} diff --git a/registry/consul/watcher.go b/registry/consul/watcher.go deleted file mode 100644 index 62829136..00000000 --- a/registry/consul/watcher.go +++ /dev/null @@ -1,290 +0,0 @@ -package consul - -import ( - "fmt" - "log" - "os" - "sync" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/api/watch" - "github.com/micro/go-micro/registry" -) - -type consulWatcher struct { - r *consulRegistry - wo registry.WatchOptions - wp *watch.Plan - watchers map[string]*watch.Plan - - next chan *registry.Result - exit chan bool - - sync.RWMutex - services map[string][]*registry.Service -} - -func newConsulWatcher(cr *consulRegistry, opts ...registry.WatchOption) (registry.Watcher, error) { - var wo registry.WatchOptions - for _, o := range opts { - o(&wo) - } - - cw := &consulWatcher{ - r: cr, - wo: wo, - exit: make(chan bool), - next: make(chan *registry.Result, 10), - watchers: make(map[string]*watch.Plan), - services: make(map[string][]*registry.Service), - } - - wp, err := watch.Parse(map[string]interface{}{"type": "services"}) - if err != nil { - return nil, err - } - - wp.Handler = cw.handle - go wp.RunWithClientAndLogger(cr.Client(), log.New(os.Stderr, "", log.LstdFlags)) - cw.wp = wp - - return cw, nil -} - -func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) { - entries, ok := data.([]*api.ServiceEntry) - if !ok { - return - } - - serviceMap := map[string]*registry.Service{} - serviceName := "" - - for _, e := range entries { - serviceName = e.Service.Service - // version is now a tag - version, _ := decodeVersion(e.Service.Tags) - // service ID is now the node id - id := e.Service.ID - // key is always the version - key := version - // address is service address - address := e.Service.Address - - // use node address - if len(address) == 0 { - address = e.Node.Address - } - - svc, ok := serviceMap[key] - if !ok { - svc = ®istry.Service{ - Endpoints: decodeEndpoints(e.Service.Tags), - Name: e.Service.Service, - Version: version, - } - serviceMap[key] = svc - } - - var del bool - - for _, check := range e.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, ®istry.Node{ - Id: id, - Address: fmt.Sprintf("%s:%d", address, e.Service.Port), - Metadata: decodeMetadata(e.Service.Tags), - }) - } - - cw.RLock() - // make a copy - rservices := make(map[string][]*registry.Service) - for k, v := range cw.services { - rservices[k] = v - } - cw.RUnlock() - - var newServices []*registry.Service - - // serviceMap is the new set of services keyed by name+version - for _, newService := range serviceMap { - // append to the new set of cached services - newServices = append(newServices, newService) - - // check if the service exists in the existing cache - oldServices, ok := rservices[serviceName] - if !ok { - // does not exist? then we're creating brand new entries - cw.next <- ®istry.Result{Action: "create", Service: newService} - continue - } - - // service exists. ok let's figure out what to update and delete version wise - action := "create" - - for _, oldService := range oldServices { - // does this version exist? - // no? then default to create - if oldService.Version != newService.Version { - continue - } - - // yes? then it's an update - action = "update" - - var nodes []*registry.Node - // check the old nodes to see if they've been deleted - for _, oldNode := range oldService.Nodes { - var seen bool - for _, newNode := range newService.Nodes { - if newNode.Id == oldNode.Id { - seen = true - break - } - } - // does the old node exist in the new set of nodes - // no? then delete that shit - if !seen { - nodes = append(nodes, oldNode) - } - } - - // it's an update rather than creation - if len(nodes) > 0 { - delService := registry.CopyService(oldService) - delService.Nodes = nodes - cw.next <- ®istry.Result{Action: "delete", Service: delService} - } - } - - cw.next <- ®istry.Result{Action: action, Service: newService} - } - - // Now check old versions that may not be in new services map - for _, old := range rservices[serviceName] { - // old version does not exist in new version map - // kill it with fire! - if _, ok := serviceMap[old.Version]; !ok { - cw.next <- ®istry.Result{Action: "delete", Service: old} - } - } - - cw.Lock() - cw.services[serviceName] = newServices - cw.Unlock() -} - -func (cw *consulWatcher) handle(idx uint64, data interface{}) { - services, ok := data.(map[string][]string) - if !ok { - return - } - - // add new watchers - for service, _ := range services { - // Filter on watch options - // wo.Service: Only watch services we care about - if len(cw.wo.Service) > 0 && service != cw.wo.Service { - continue - } - - if _, ok := cw.watchers[service]; ok { - continue - } - wp, err := watch.Parse(map[string]interface{}{ - "type": "service", - "service": service, - }) - if err == nil { - wp.Handler = cw.serviceHandler - go wp.RunWithClientAndLogger(cw.r.Client(), log.New(os.Stderr, "", log.LstdFlags)) - cw.watchers[service] = wp - cw.next <- ®istry.Result{Action: "create", Service: ®istry.Service{Name: service}} - } - } - - cw.RLock() - // make a copy - rservices := make(map[string][]*registry.Service) - for k, v := range cw.services { - rservices[k] = v - } - cw.RUnlock() - - // remove unknown services from registry - // save the things we want to delete - deleted := make(map[string][]*registry.Service) - - for service, _ := range rservices { - if _, ok := services[service]; !ok { - cw.Lock() - // save this before deleting - deleted[service] = cw.services[service] - delete(cw.services, service) - cw.Unlock() - } - } - - // remove unknown services from watchers - for service, w := range cw.watchers { - if _, ok := services[service]; !ok { - w.Stop() - delete(cw.watchers, service) - for _, oldService := range deleted[service] { - // send a delete for the service nodes that we're removing - cw.next <- ®istry.Result{Action: "delete", Service: oldService} - } - // sent the empty list as the last resort to indicate to delete the entire service - cw.next <- ®istry.Result{Action: "delete", Service: ®istry.Service{Name: service}} - } - } -} - -func (cw *consulWatcher) Next() (*registry.Result, error) { - select { - case <-cw.exit: - return nil, registry.ErrWatcherStopped - case r, ok := <-cw.next: - if !ok { - return nil, registry.ErrWatcherStopped - } - return r, nil - } - // NOTE: This is a dead code path: e.g. it will never be reached - // as we return in all previous code paths never leading to this return - return nil, registry.ErrWatcherStopped -} - -func (cw *consulWatcher) Stop() { - select { - case <-cw.exit: - return - default: - close(cw.exit) - if cw.wp == nil { - return - } - cw.wp.Stop() - - // drain results - for { - select { - case <-cw.next: - default: - return - } - } - } -} diff --git a/registry/consul/watcher_test.go b/registry/consul/watcher_test.go deleted file mode 100644 index 19e0cb80..00000000 --- a/registry/consul/watcher_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package consul - -import ( - "testing" - - "github.com/hashicorp/consul/api" - "github.com/micro/go-micro/registry" -) - -func TestHealthyServiceHandler(t *testing.T) { - watcher := newWatcher() - serviceEntry := newServiceEntry( - "node-name", "node-address", "service-name", "v1.0.0", - []*api.HealthCheck{ - newHealthCheck("node-name", "service-name", "passing"), - }, - ) - - watcher.serviceHandler(1234, []*api.ServiceEntry{serviceEntry}) - - if len(watcher.services["service-name"][0].Nodes) != 1 { - t.Errorf("Expected length of the service nodes to be 1") - } -} - -func TestUnhealthyServiceHandler(t *testing.T) { - watcher := newWatcher() - serviceEntry := newServiceEntry( - "node-name", "node-address", "service-name", "v1.0.0", - []*api.HealthCheck{ - newHealthCheck("node-name", "service-name", "critical"), - }, - ) - - watcher.serviceHandler(1234, []*api.ServiceEntry{serviceEntry}) - - if len(watcher.services["service-name"][0].Nodes) != 0 { - t.Errorf("Expected length of the service nodes to be 0") - } -} - -func TestUnhealthyNodeServiceHandler(t *testing.T) { - watcher := newWatcher() - serviceEntry := newServiceEntry( - "node-name", "node-address", "service-name", "v1.0.0", - []*api.HealthCheck{ - newHealthCheck("node-name", "service-name", "passing"), - newHealthCheck("node-name", "serfHealth", "critical"), - }, - ) - - watcher.serviceHandler(1234, []*api.ServiceEntry{serviceEntry}) - - if len(watcher.services["service-name"][0].Nodes) != 0 { - t.Errorf("Expected length of the service nodes to be 0") - } -} - -func newWatcher() *consulWatcher { - return &consulWatcher{ - exit: make(chan bool), - next: make(chan *registry.Result, 10), - services: make(map[string][]*registry.Service), - } -} - -func newHealthCheck(node, name, status string) *api.HealthCheck { - return &api.HealthCheck{ - Node: node, - Name: name, - Status: status, - ServiceName: name, - } -} - -func newServiceEntry(node, address, name, version string, checks []*api.HealthCheck) *api.ServiceEntry { - return &api.ServiceEntry{ - Node: &api.Node{Node: node, Address: name}, - Service: &api.AgentService{ - Service: name, - Address: address, - Tags: encodeVersion(version), - }, - Checks: checks, - } -} diff --git a/registry/encoding_test.go b/registry/encoding_test.go index 89acd980..ff9ae97e 100644 --- a/registry/encoding_test.go +++ b/registry/encoding_test.go @@ -6,13 +6,13 @@ import ( func TestEncoding(t *testing.T) { testData := []*mdnsTxt{ - &mdnsTxt{ + { Version: "1.0.0", Metadata: map[string]string{ "foo": "bar", }, Endpoints: []*Endpoint{ - &Endpoint{ + { Name: "endpoint1", Request: &Value{ Name: "request", diff --git a/registry/etcd/etcd.go b/registry/etcd/etcd.go new file mode 100644 index 00000000..a48932fc --- /dev/null +++ b/registry/etcd/etcd.go @@ -0,0 +1,396 @@ +// Package etcd provides an etcd service registry +package etcd + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "net" + "path" + "sort" + "strings" + "sync" + "time" + + "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes" + "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/util/log" + hash "github.com/mitchellh/hashstructure" +) + +var ( + prefix = "/micro/registry/" +) + +type etcdRegistry struct { + client *clientv3.Client + options registry.Options + + sync.RWMutex + register map[string]uint64 + leases map[string]clientv3.LeaseID +} + +func NewRegistry(opts ...registry.Option) registry.Registry { + e := &etcdRegistry{ + options: registry.Options{}, + register: make(map[string]uint64), + leases: make(map[string]clientv3.LeaseID), + } + configure(e, opts...) + return e +} + +func configure(e *etcdRegistry, opts ...registry.Option) error { + config := clientv3.Config{ + Endpoints: []string{"127.0.0.1:2379"}, + } + + for _, o := range opts { + o(&e.options) + } + + if e.options.Timeout == 0 { + e.options.Timeout = 5 * time.Second + } + + if e.options.Secure || e.options.TLSConfig != nil { + tlsConfig := e.options.TLSConfig + if tlsConfig == nil { + tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + + config.TLS = tlsConfig + } + + if e.options.Context != nil { + u, ok := e.options.Context.Value(authKey{}).(*authCreds) + if ok { + config.Username = u.Username + config.Password = u.Password + } + } + + var cAddrs []string + + for _, address := range e.options.Addrs { + if len(address) == 0 { + continue + } + addr, port, err := net.SplitHostPort(address) + if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { + port = "2379" + addr = address + cAddrs = append(cAddrs, net.JoinHostPort(addr, port)) + } else if err == nil { + cAddrs = append(cAddrs, net.JoinHostPort(addr, port)) + } + } + + // if we got addrs then we'll update + if len(cAddrs) > 0 { + config.Endpoints = cAddrs + } + + cli, err := clientv3.New(config) + if err != nil { + return err + } + e.client = cli + return nil +} + +func encode(s *registry.Service) string { + b, _ := json.Marshal(s) + return string(b) +} + +func decode(ds []byte) *registry.Service { + var s *registry.Service + json.Unmarshal(ds, &s) + return s +} + +func nodePath(s, id string) string { + service := strings.Replace(s, "/", "-", -1) + node := strings.Replace(id, "/", "-", -1) + return path.Join(prefix, service, node) +} + +func servicePath(s string) string { + return path.Join(prefix, strings.Replace(s, "/", "-", -1)) +} + +func (e *etcdRegistry) Init(opts ...registry.Option) error { + return configure(e, opts...) +} + +func (e *etcdRegistry) Options() registry.Options { + return e.options +} + +func (e *etcdRegistry) registerNode(s *registry.Service, node *registry.Node, opts ...registry.RegisterOption) error { + if len(s.Nodes) == 0 { + return errors.New("Require at least one node") + } + + // check existing lease cache + e.RLock() + leaseID, ok := e.leases[s.Name+node.Id] + e.RUnlock() + + if !ok { + // missing lease, check if the key exists + ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) + defer cancel() + + // look for the existing key + rsp, err := e.client.Get(ctx, nodePath(s.Name, node.Id), clientv3.WithSerializable()) + if err != nil { + return err + } + + // get the existing lease + for _, kv := range rsp.Kvs { + if kv.Lease > 0 { + leaseID = clientv3.LeaseID(kv.Lease) + + // decode the existing node + srv := decode(kv.Value) + if srv == nil || len(srv.Nodes) == 0 { + continue + } + + // create hash of service; uint64 + h, err := hash.Hash(srv.Nodes[0], nil) + if err != nil { + continue + } + + // save the info + e.Lock() + e.leases[s.Name+node.Id] = leaseID + e.register[s.Name+node.Id] = h + e.Unlock() + + break + } + } + } + + var leaseNotFound bool + + // renew the lease if it exists + if leaseID > 0 { + log.Tracef("Renewing existing lease for %s %d", s.Name, leaseID) + if _, err := e.client.KeepAliveOnce(context.TODO(), leaseID); err != nil { + if err != rpctypes.ErrLeaseNotFound { + return err + } + + log.Tracef("Lease not found for %s %d", s.Name, leaseID) + // lease not found do register + leaseNotFound = true + } + } + + // create hash of service; uint64 + h, err := hash.Hash(node, nil) + if err != nil { + return err + } + + // get existing hash for the service node + e.Lock() + v, ok := e.register[s.Name+node.Id] + e.Unlock() + + // the service is unchanged, skip registering + if ok && v == h && !leaseNotFound { + log.Tracef("Service %s node %s unchanged skipping registration", s.Name, node.Id) + return nil + } + + service := ®istry.Service{ + Name: s.Name, + Version: s.Version, + Metadata: s.Metadata, + Endpoints: s.Endpoints, + Nodes: []*registry.Node{node}, + } + + var options registry.RegisterOptions + for _, o := range opts { + o(&options) + } + + ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) + defer cancel() + + var lgr *clientv3.LeaseGrantResponse + if options.TTL.Seconds() > 0 { + // get a lease used to expire keys since we have a ttl + lgr, err = e.client.Grant(ctx, int64(options.TTL.Seconds())) + if err != nil { + return err + } + } + + log.Tracef("Registering %s id %s with lease %v and ttl %v", service.Name, node.Id, lgr, options.TTL) + // create an entry for the node + if lgr != nil { + _, err = e.client.Put(ctx, nodePath(service.Name, node.Id), encode(service), clientv3.WithLease(lgr.ID)) + } else { + _, err = e.client.Put(ctx, nodePath(service.Name, node.Id), encode(service)) + } + if err != nil { + return err + } + + e.Lock() + // save our hash of the service + e.register[s.Name+node.Id] = h + // save our leaseID of the service + if lgr != nil { + e.leases[s.Name+node.Id] = lgr.ID + } + e.Unlock() + + return nil +} + +func (e *etcdRegistry) Deregister(s *registry.Service) error { + if len(s.Nodes) == 0 { + return errors.New("Require at least one node") + } + + for _, node := range s.Nodes { + e.Lock() + // delete our hash of the service + delete(e.register, s.Name+node.Id) + // delete our lease of the service + delete(e.leases, s.Name+node.Id) + e.Unlock() + + ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) + defer cancel() + + log.Tracef("Deregistering %s id %s", s.Name, node.Id) + _, err := e.client.Delete(ctx, nodePath(s.Name, node.Id)) + if err != nil { + return err + } + } + + return nil +} + +func (e *etcdRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error { + if len(s.Nodes) == 0 { + return errors.New("Require at least one node") + } + + var gerr error + + // register each node individually + for _, node := range s.Nodes { + err := e.registerNode(s, node, opts...) + if err != nil { + gerr = err + } + } + + return gerr +} + +func (e *etcdRegistry) GetService(name string) ([]*registry.Service, error) { + ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) + defer cancel() + + rsp, err := e.client.Get(ctx, servicePath(name)+"/", clientv3.WithPrefix(), clientv3.WithSerializable()) + if err != nil { + return nil, err + } + + if len(rsp.Kvs) == 0 { + return nil, registry.ErrNotFound + } + + serviceMap := map[string]*registry.Service{} + + for _, n := range rsp.Kvs { + if sn := decode(n.Value); sn != nil { + s, ok := serviceMap[sn.Version] + if !ok { + s = ®istry.Service{ + Name: sn.Name, + Version: sn.Version, + Metadata: sn.Metadata, + Endpoints: sn.Endpoints, + } + serviceMap[s.Version] = s + } + + s.Nodes = append(s.Nodes, sn.Nodes...) + } + } + + services := make([]*registry.Service, 0, len(serviceMap)) + for _, service := range serviceMap { + services = append(services, service) + } + + return services, nil +} + +func (e *etcdRegistry) ListServices() ([]*registry.Service, error) { + versions := make(map[string]*registry.Service) + + ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) + defer cancel() + + rsp, err := e.client.Get(ctx, prefix, clientv3.WithPrefix(), clientv3.WithSerializable()) + if err != nil { + return nil, err + } + + if len(rsp.Kvs) == 0 { + return []*registry.Service{}, nil + } + + for _, n := range rsp.Kvs { + sn := decode(n.Value) + if sn == nil { + continue + } + v, ok := versions[sn.Name+sn.Version] + if !ok { + versions[sn.Name+sn.Version] = sn + continue + } + // append to service:version nodes + v.Nodes = append(v.Nodes, sn.Nodes...) + } + + services := make([]*registry.Service, 0, len(versions)) + for _, service := range versions { + services = append(services, service) + } + + // sort the services + sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) + + return services, nil +} + +func (e *etcdRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { + return newEtcdWatcher(e, e.options.Timeout, opts...) +} + +func (e *etcdRegistry) String() string { + return "etcd" +} diff --git a/registry/etcd/options.go b/registry/etcd/options.go new file mode 100644 index 00000000..46bacc09 --- /dev/null +++ b/registry/etcd/options.go @@ -0,0 +1,24 @@ +package etcd + +import ( + "context" + + "github.com/micro/go-micro/registry" +) + +type authKey struct{} + +type authCreds struct { + Username string + Password string +} + +// Auth allows you to specify username/password +func Auth(username, password string) registry.Option { + return func(o *registry.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, authKey{}, &authCreds{Username: username, Password: password}) + } +} diff --git a/registry/etcd/watcher.go b/registry/etcd/watcher.go new file mode 100644 index 00000000..4eef76ba --- /dev/null +++ b/registry/etcd/watcher.go @@ -0,0 +1,88 @@ +package etcd + +import ( + "context" + "errors" + "time" + + "github.com/coreos/etcd/clientv3" + "github.com/micro/go-micro/registry" +) + +type etcdWatcher struct { + stop chan bool + w clientv3.WatchChan + client *clientv3.Client + timeout time.Duration +} + +func newEtcdWatcher(r *etcdRegistry, timeout time.Duration, opts ...registry.WatchOption) (registry.Watcher, error) { + var wo registry.WatchOptions + for _, o := range opts { + o(&wo) + } + + ctx, cancel := context.WithCancel(context.Background()) + stop := make(chan bool, 1) + + go func() { + <-stop + cancel() + }() + + watchPath := prefix + if len(wo.Service) > 0 { + watchPath = servicePath(wo.Service) + "/" + } + + return &etcdWatcher{ + stop: stop, + w: r.client.Watch(ctx, watchPath, clientv3.WithPrefix(), clientv3.WithPrevKV()), + client: r.client, + timeout: timeout, + }, nil +} + +func (ew *etcdWatcher) Next() (*registry.Result, error) { + for wresp := range ew.w { + if wresp.Err() != nil { + return nil, wresp.Err() + } + for _, ev := range wresp.Events { + service := decode(ev.Kv.Value) + var action string + + switch ev.Type { + case clientv3.EventTypePut: + if ev.IsCreate() { + action = "create" + } else if ev.IsModify() { + action = "update" + } + case clientv3.EventTypeDelete: + action = "delete" + + // get service from prevKv + service = decode(ev.PrevKv.Value) + } + + if service == nil { + continue + } + return ®istry.Result{ + Action: action, + Service: service, + }, nil + } + } + return nil, errors.New("could not get next") +} + +func (ew *etcdWatcher) Stop() { + select { + case <-ew.stop: + return + default: + close(ew.stop) + } +} diff --git a/registry/gossip/README.md b/registry/gossip/README.md deleted file mode 100644 index bfeca969..00000000 --- a/registry/gossip/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Gossip Registry - -Gossip is a zero dependency registry which uses github.com/hashicorp/memberlist to broadcast registry information -via the SWIM protocol. - -## Usage - -Start with the registry flag or env var - -```bash -MICRO_REGISTRY=gossip go run service.go -``` - -On startup you'll see something like - -```bash -2018/12/06 18:17:48 Registry Listening on 192.168.1.65:56390 -``` - -To join this gossip ring set the registry address using flag or env var - -```bash -MICRO_REGISTRY_ADDRESS=192.168.1.65:56390 -``` diff --git a/registry/gossip/gossip.go b/registry/gossip/gossip.go deleted file mode 100644 index 24006438..00000000 --- a/registry/gossip/gossip.go +++ /dev/null @@ -1,843 +0,0 @@ -// Package gossip provides a gossip registry based on hashicorp/memberlist -package gossip - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/golang/protobuf/proto" - "github.com/google/uuid" - "github.com/hashicorp/memberlist" - "github.com/micro/go-micro/registry" - pb "github.com/micro/go-micro/registry/gossip/proto" - log "github.com/micro/go-micro/util/log" - "github.com/mitchellh/hashstructure" -) - -// use registry.Result int32 values after it switches from string to int32 types -// type actionType int32 -// type updateType int32 - -const ( - 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 { - update *pb.Update - notify chan<- struct{} -} - -type delegate struct { - queue *memberlist.TransmitLimitedQueue - updates chan *update -} - -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 - - watchers map[string]chan *registry.Result - - mtu int - addrs []string - members map[string]int32 - done chan bool -} - -type update struct { - Update *pb.Update - Service *registry.Service - sync chan *registry.Service -} - -type updates struct { - sync.RWMutex - services map[uint64]*update -} - -var ( - // You should change this if using secure - DefaultSecret = []byte("micro-gossip-key") // exactly 16 bytes - ExpiryTick = time.Second * 1 // needs to be smaller than registry.RegisterTTL - MaxPacketSize = 512 -) - -func configure(g *gossipRegistry, opts ...registry.Option) error { - // loop through address list and get valid entries - addrs := func(curAddrs []string) []string { - var newAddrs []string - for _, addr := range curAddrs { - if trimAddr := strings.TrimSpace(addr); len(trimAddr) > 0 { - newAddrs = append(newAddrs, trimAddr) - } - } - return newAddrs - } - - // current address list - curAddrs := addrs(g.options.Addrs) - - // parse options - for _, o := range opts { - o(&g.options) - } - - // new address list - newAddrs := addrs(g.options.Addrs) - - // no new nodes and existing member. no configure - if (len(newAddrs) == len(curAddrs)) && g.member != nil { - return nil - } - - // shutdown old member - g.Stop() - - // lock internals - g.Lock() - - // new done chan - g.done = make(chan bool) - - // replace addresses - curAddrs = newAddrs - - // 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 - - // set config from options - if config, ok := g.options.Context.Value(configKey{}).(*memberlist.Config); ok && config != nil { - c = config - } - - // set address - if address, ok := g.options.Context.Value(addressKey{}).(string); ok { - host, port, err := net.SplitHostPort(address) - if err == nil { - p, err := strconv.Atoi(port) - if err == nil { - c.BindPort = p - } - c.BindAddr = host - } - } else { - // set bind to random port - c.BindPort = 0 - } - - // set the advertise address - if advertise, ok := g.options.Context.Value(advertiseKey{}).(string); ok { - host, port, err := net.SplitHostPort(advertise) - if err == nil { - p, err := strconv.Atoi(port) - if err == nil { - c.AdvertisePort = p - } - c.AdvertiseAddr = host - } - } - - // machine hostname - hostname, _ := os.Hostname() - - // set the name - c.Name = strings.Join([]string{"micro", hostname, uuid.New().String()}, "-") - - // set a secret key if secure - if g.options.Secure { - k, ok := g.options.Context.Value(secretKey{}).([]byte) - if !ok { - // use the default secret - k = DefaultSecret - } - c.SecretKey = k - } - - // set connect retry - if v, ok := g.options.Context.Value(connectRetryKey{}).(bool); ok && v { - g.connectRetry = true - } - - // set connect timeout - if td, ok := g.options.Context.Value(connectTimeoutKey{}).(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 - } - - if len(curAddrs) > 0 { - for _, addr := range curAddrs { - g.members[addr] = nodeActionUnknown - } - } - - g.tcpInterval = c.PushPullInterval - g.addrs = curAddrs - g.queue = queue - g.member = m - g.interval = c.GossipInterval - - g.Unlock() - - log.Logf("[gossip] Registry Listening on %s", m.LocalNode().Address()) - - // try connect - return g.connect(curAddrs) -} - -func (*broadcast) UniqueBroadcast() {} - -func (b *broadcast) Invalidates(other memberlist.Broadcast) bool { - return false -} - -func (b *broadcast) Message() []byte { - up, err := proto.Marshal(b.update) - 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 -} - -func (b *broadcast) Finished() { - if b.notify != nil { - close(b.notify) - } -} - -func (d *delegate) NodeMeta(limit int) []byte { - return []byte{} -} - -func (d *delegate) NotifyMsg(b []byte) { - if len(b) == 0 { - return - } - - go func() { - up := new(pb.Update) - if err := proto.Unmarshal(b, up); err != nil { - return - } - - // only process service action - if up.Type != updateTypeService { - return - } - - var service *registry.Service - - switch up.Metadata["Content-Type"] { - case "application/json": - if err := json.Unmarshal(up.Data, &service); err != nil { - return - } - // no other content type - default: - return - } - - // send update - d.updates <- &update{ - Update: up, - Service: service, - } - }() -} - -func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte { - return d.queue.GetBroadcasts(overhead, limit) -} - -func (d *delegate) LocalState(join bool) []byte { - if !join { - return []byte{} - } - - syncCh := make(chan *registry.Service, 1) - services := map[string][]*registry.Service{} - - d.updates <- &update{ - Update: &pb.Update{ - Action: actionTypeSync, - }, - sync: syncCh, - } - - for srv := range syncCh { - services[srv.Name] = append(services[srv.Name], srv) - } - - b, _ := json.Marshal(services) - return b -} - -func (d *delegate) MergeRemoteState(buf []byte, join bool) { - if len(buf) == 0 { - return - } - if !join { - return - } - - var services map[string][]*registry.Service - if err := json.Unmarshal(buf, &services); err != nil { - return - } - for _, service := range services { - for _, srv := range service { - d.updates <- &update{ - Update: &pb.Update{Action: actionTypeCreate}, - Service: srv, - sync: nil, - } - } - } -} - -func (g *gossipRegistry) connect(addrs []string) 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] connect timeout %v", g.addrs) - // got a tick, try to connect - case <-ticker.C: - if _, err := fn(); err == nil { - log.Logf("[gossip] connect success for %v", g.addrs) - return nil - } else { - log.Logf("[gossip] connect failed for %v", g.addrs) - } - } - } - - return nil -} - -func (g *gossipRegistry) publish(action string, services []*registry.Service) { - g.RLock() - for _, sub := range g.watchers { - go func(sub chan *registry.Result) { - for _, service := range services { - sub <- ®istry.Result{Action: action, Service: service} - } - }(sub) - } - g.RUnlock() -} - -func (g *gossipRegistry) subscribe() (chan *registry.Result, chan bool) { - next := make(chan *registry.Result, 10) - exit := make(chan bool) - - id := uuid.New().String() - - g.Lock() - g.watchers[id] = next - g.Unlock() - - go func() { - <-exit - g.Lock() - delete(g.watchers, id) - close(next) - g.Unlock() - }() - - return next, exit -} - -func (g *gossipRegistry) Stop() error { - select { - case <-g.done: - return nil - default: - close(g.done) - g.Lock() - if g.member != nil { - g.member.Leave(g.interval * 2) - g.member.Shutdown() - g.member = nil - } - g.Unlock() - } - return nil -} - -// connectLoop attempts to reconnect to the memberlist -func (g *gossipRegistry) connectLoop() { - // try every second - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-g.done: - return - case <-g.options.Context.Done(): - g.Stop() - return - case <-ticker.C: - var addrs []string - - g.RLock() - - // only process if we have a memberlist - if g.member == nil { - g.RUnlock() - continue - } - - // self - local := g.member.LocalNode().Address() - - // operate on each member - for node, action := range g.members { - switch action { - // process leave event - case nodeActionLeave: - // don't process self - if node == local { - continue - } - addrs = append(addrs, node) - } - } - - g.RUnlock() - - // connect to all the members - // TODO: only connect to new members - if len(addrs) > 0 { - g.connect(addrs) - } - } - } -} - -func (g *gossipRegistry) expiryLoop(updates *updates) { - ticker := time.NewTicker(ExpiryTick) - defer ticker.Stop() - - g.RLock() - done := g.done - g.RUnlock() - - for { - select { - case <-done: - return - case <-ticker.C: - now := uint64(time.Now().UnixNano()) - - updates.Lock() - - // process all the updates - for k, v := range updates.services { - // check if expiry time has passed - if d := (v.Update.Expires); d < now { - // delete from records - delete(updates.services, k) - // set to delete - v.Update.Action = actionTypeDelete - // fire a new update - g.updates <- v - } - } - - updates.Unlock() - } - } -} - -// process member events -func (g *gossipRegistry) eventLoop() { - g.RLock() - done := g.done - g.RUnlock() - for { - select { - // return when done - case <-done: - return - case ev := <-g.events: - // TODO: nonblocking update - g.Lock() - if _, ok := g.members[ev.node]; ok { - g.members[ev.node] = ev.action - } - g.Unlock() - } - } -} - -func (g *gossipRegistry) run() { - updates := &updates{ - services: make(map[uint64]*update), - } - - // expiry loop - go g.expiryLoop(updates) - - // event loop - go g.eventLoop() - - g.RLock() - // connect loop - if g.connectRetry { - go g.connectLoop() - } - g.RUnlock() - - // process the updates - for u := range g.updates { - switch u.Update.Action { - case actionTypeCreate: - g.Lock() - if service, ok := g.services[u.Service.Name]; !ok { - g.services[u.Service.Name] = []*registry.Service{u.Service} - - } else { - g.services[u.Service.Name] = registry.Merge(service, []*registry.Service{u.Service}) - } - g.Unlock() - - // publish update to watchers - 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 { - // create a hash of this service - if hash, err := hashstructure.Hash(u.Service, nil); err == nil { - updates.Lock() - updates.services[hash] = u - updates.Unlock() - } - } - case actionTypeDelete: - g.Lock() - if service, ok := g.services[u.Service.Name]; ok { - if services := registry.Remove(service, []*registry.Service{u.Service}); len(services) == 0 { - delete(g.services, u.Service.Name) - } else { - g.services[u.Service.Name] = services - } - } - g.Unlock() - - // publish update to watchers - go g.publish(actionTypeString(actionTypeDelete), []*registry.Service{u.Service}) - - // delete from expiry checks - if hash, err := hashstructure.Hash(u.Service, nil); err == nil { - updates.Lock() - delete(updates.services, hash) - updates.Unlock() - } - case actionTypeSync: - // no sync channel provided - if u.sync == nil { - continue - } - - g.RLock() - - // push all services through the sync chan - for _, service := range g.services { - for _, srv := range service { - u.sync <- srv - } - - // publish to watchers - go g.publish(actionTypeString(actionTypeCreate), service) - } - - g.RUnlock() - - // close the sync chan - close(u.sync) - } - } -} - -func (g *gossipRegistry) Init(opts ...registry.Option) error { - return configure(g, opts...) -} - -func (g *gossipRegistry) Options() registry.Options { - return g.options -} - -func (g *gossipRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error { - b, err := json.Marshal(s) - if err != nil { - return err - } - - g.Lock() - if service, ok := g.services[s.Name]; !ok { - g.services[s.Name] = []*registry.Service{s} - } else { - g.services[s.Name] = registry.Merge(service, []*registry.Service{s}) - } - g.Unlock() - - var options registry.RegisterOptions - for _, o := range opts { - o(&options) - } - - if options.TTL == 0 && g.tcpInterval == 0 { - return fmt.Errorf("Require register TTL or interval for memberlist.Config") - } - - up := &pb.Update{ - Expires: uint64(time.Now().Add(options.TTL).UnixNano()), - Action: actionTypeCreate, - Type: updateTypeService, - Metadata: map[string]string{ - "Content-Type": "application/json", - }, - Data: b, - } - - g.queue.QueueBroadcast(&broadcast{ - update: up, - notify: nil, - }) - - // send update to local watchers - g.updates <- &update{ - Update: up, - Service: s, - } - - // wait - <-time.After(g.interval * 2) - - return nil -} - -func (g *gossipRegistry) Deregister(s *registry.Service) error { - b, err := json.Marshal(s) - if err != nil { - return err - } - - g.Lock() - if service, ok := g.services[s.Name]; ok { - if services := registry.Remove(service, []*registry.Service{s}); len(services) == 0 { - delete(g.services, s.Name) - } else { - g.services[s.Name] = services - } - } - g.Unlock() - - up := &pb.Update{ - Action: actionTypeDelete, - Type: updateTypeService, - Metadata: map[string]string{ - "Content-Type": "application/json", - }, - Data: b, - } - - g.queue.QueueBroadcast(&broadcast{ - update: up, - notify: nil, - }) - - // send update to local watchers - g.updates <- &update{ - Update: up, - Service: s, - } - - // wait - <-time.After(g.interval * 2) - - return nil -} - -func (g *gossipRegistry) GetService(name string) ([]*registry.Service, error) { - g.RLock() - service, ok := g.services[name] - g.RUnlock() - if !ok { - return nil, registry.ErrNotFound - } - return service, nil -} - -func (g *gossipRegistry) ListServices() ([]*registry.Service, error) { - var services []*registry.Service - g.RLock() - for _, service := range g.services { - services = append(services, service...) - } - g.RUnlock() - return services, nil -} - -func (g *gossipRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { - n, e := g.subscribe() - return newGossipWatcher(n, e, opts...) -} - -func (g *gossipRegistry) String() string { - return "gossip" -} - -func NewRegistry(opts ...registry.Option) registry.Registry { - g := &gossipRegistry{ - options: registry.Options{ - Context: context.Background(), - }, - done: make(chan bool), - events: make(chan *event, 100), - updates: make(chan *update, 100), - services: make(map[string][]*registry.Service), - watchers: make(map[string]chan *registry.Result), - members: make(map[string]int32), - } - // run the updater - go g.run() - - // configure the gossiper - if err := configure(g, opts...); err != nil { - log.Fatalf("[gossip] Error configuring registry: %v", err) - } - // wait for setup - <-time.After(g.interval * 2) - - return g -} diff --git a/registry/gossip/gossip_test.go b/registry/gossip/gossip_test.go deleted file mode 100644 index 9154d08d..00000000 --- a/registry/gossip/gossip_test.go +++ /dev/null @@ -1,214 +0,0 @@ -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 TestGossipRegistryBroadcast(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.1:54322"), registry.Addrs("127.0.0.1:54321")) - - defer r1.(*gossipRegistry).Stop() - defer r2.(*gossipRegistry).Stop() - - svc1 := ®istry.Service{Name: "service.1", Version: "0.0.0.1"} - svc2 := ®istry.Service{Name: "service.2", Version: "0.0.0.2"} - - if err := r1.Register(svc1, registry.RegisterTTL(10*time.Second)); err != nil { - t.Fatal(err) - } - if err := r2.Register(svc2, registry.RegisterTTL(10*time.Second)); err != nil { - t.Fatal(err) - } - - var found bool - svcs, err := r1.ListServices() - if err != nil { - t.Fatal(err) - } - - for _, svc := range svcs { - if svc.Name == "service.2" { - found = true - } - } - if !found { - t.Fatalf("[gossip registry] service.2 not found in r1, broadcast not work") - } - - found = false - - svcs, err = r2.ListServices() - if err != nil { - t.Fatal(err) - } - - for _, svc := range svcs { - if svc.Name == "service.1" { - found = true - } - } - - if !found { - t.Fatalf("[gossip registry] broadcast failed: service.1 not found in r2") - } - - if err := r1.Deregister(svc1); err != nil { - t.Fatal(err) - } - if err := r2.Deregister(svc2); err != nil { - t.Fatal(err) - } - -} -func TestGossipRegistryRetry(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.1:54322"), registry.Addrs("127.0.0.1:54321")) - - defer r1.(*gossipRegistry).Stop() - defer r2.(*gossipRegistry).Stop() - - svc1 := ®istry.Service{Name: "service.1", Version: "0.0.0.1"} - svc2 := ®istry.Service{Name: "service.2", 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 == "service.1" { - found = true - } - } - - if !found { - t.Fatalf("[gossip registry] broadcast failed: service.1 not found in r2") - } - - if err = r1.(*gossipRegistry).Stop(); err != nil { - t.Fatalf("[gossip registry] failed to stop 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 == "service.1" { - found = true - } - } - - if found { - t.Fatalf("[gossip registry] service.1 found in r2") - } - - if tr := os.Getenv("TRAVIS"); len(tr) > 0 { - t.Logf("[gossip registry] skip test on travis") - t.Skip() - return - } - - r1 = newRegistry(Config(mc1), Address("127.0.0.1:54321")) - <-time.After(2 * time.Second) - - found = false - svcs, err = r2.ListServices() - if err != nil { - t.Fatal(err) - } - - for _, svc := range svcs { - if svc.Name == "service.1" { - found = true - } - } - - if !found { - t.Fatalf("[gossip registry] connect retry failed: service.1 not found in r2") - } - - 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() -} diff --git a/registry/gossip/options.go b/registry/gossip/options.go deleted file mode 100644 index cf9dc9dc..00000000 --- a/registry/gossip/options.go +++ /dev/null @@ -1,58 +0,0 @@ -package gossip - -import ( - "context" - "time" - - "github.com/hashicorp/memberlist" - "github.com/micro/go-micro/registry" -) - -type secretKey struct{} -type addressKey struct{} -type configKey struct{} -type advertiseKey struct{} -type connectTimeoutKey struct{} -type connectRetryKey struct{} - -// helper for setting registry options -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) - } -} - -// 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 setRegistryOption(secretKey{}, k) -} - -// Address to bind to - host:port -func Address(a string) registry.Option { - return setRegistryOption(addressKey{}, a) -} - -// Config sets *memberlist.Config for configuring gossip -func Config(c *memberlist.Config) registry.Option { - return setRegistryOption(configKey{}, c) -} - -// The address to advertise for other gossip members to connect to - host:port -func Advertise(a string) registry.Option { - return setRegistryOption(advertiseKey{}, a) -} - -// ConnectTimeout sets the registry connect timeout. Use -1 to specify infinite timeout -func ConnectTimeout(td time.Duration) registry.Option { - return setRegistryOption(connectTimeoutKey{}, td) -} - -// ConnectRetry enables reconnect to registry then connection closed, -// use with ConnectTimeout to specify how long retry -func ConnectRetry(v bool) registry.Option { - return setRegistryOption(connectRetryKey{}, v) -} diff --git a/registry/gossip/proto/gossip.micro.go b/registry/gossip/proto/gossip.micro.go deleted file mode 100644 index fe9796d9..00000000 --- a/registry/gossip/proto/gossip.micro.go +++ /dev/null @@ -1,28 +0,0 @@ -// Code generated by protoc-gen-micro. DO NOT EDIT. -// source: github.com/micro/go-micro/registry/gossip/proto/gossip.proto - -/* -Package gossip is a generated protocol buffer package. - -It is generated from these files: - github.com/micro/go-micro/registry/gossip/proto/gossip.proto - -It has these top-level messages: - Update -*/ -package gossip - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// 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.ProtoPackageIsVersion2 // please upgrade the proto package diff --git a/registry/gossip/proto/gossip.pb.go b/registry/gossip/proto/gossip.pb.go deleted file mode 100644 index e5d692fc..00000000 --- a/registry/gossip/proto/gossip.pb.go +++ /dev/null @@ -1,127 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: github.com/micro/go-micro/registry/gossip/proto/gossip.proto - -package gossip - -import ( - fmt "fmt" - math "math" - - proto "github.com/golang/protobuf/proto" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// 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.ProtoPackageIsVersion2 // please upgrade the proto package - -// Update is the message broadcast -type Update struct { - // time to live for entry - 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; - Data []byte `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Update) Reset() { *m = Update{} } -func (m *Update) String() string { return proto.CompactTextString(m) } -func (*Update) ProtoMessage() {} -func (*Update) Descriptor() ([]byte, []int) { - return fileDescriptor_18cba623e76e57f3, []int{0} -} - -func (m *Update) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Update.Unmarshal(m, b) -} -func (m *Update) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Update.Marshal(b, m, deterministic) -} -func (m *Update) XXX_Merge(src proto.Message) { - xxx_messageInfo_Update.Merge(m, src) -} -func (m *Update) XXX_Size() int { - return xxx_messageInfo_Update.Size(m) -} -func (m *Update) XXX_DiscardUnknown() { - xxx_messageInfo_Update.DiscardUnknown(m) -} - -var xxx_messageInfo_Update proto.InternalMessageInfo - -func (m *Update) GetExpires() uint64 { - if m != nil { - return m.Expires - } - return 0 -} - -func (m *Update) GetType() int32 { - if m != nil { - return m.Type - } - return 0 -} - -func (m *Update) GetAction() int32 { - if m != nil { - return m.Action - } - return 0 -} - -func (m *Update) GetMetadata() map[string]string { - if m != nil { - return m.Metadata - } - return nil -} - -func (m *Update) GetData() []byte { - if m != nil { - return m.Data - } - return nil -} - -func init() { - proto.RegisterType((*Update)(nil), "gossip.Update") - proto.RegisterMapType((map[string]string)(nil), "gossip.Update.MetadataEntry") -} - -func init() { - proto.RegisterFile("github.com/micro/go-micro/registry/gossip/proto/gossip.proto", fileDescriptor_18cba623e76e57f3) -} - -var fileDescriptor_18cba623e76e57f3 = []byte{ - // 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, -} diff --git a/registry/gossip/proto/gossip.proto b/registry/gossip/proto/gossip.proto deleted file mode 100644 index 21acaaa5..00000000 --- a/registry/gossip/proto/gossip.proto +++ /dev/null @@ -1,17 +0,0 @@ -syntax = "proto3"; - -package gossip; - -// Update is the message broadcast -message Update { - // time to live for entry - uint64 expires = 1; - // type of update - int32 type = 2; - // what action is taken - int32 action = 3; - // any other associated metadata about the data - map metadata = 6; - // the payload data; - bytes data = 7; -} diff --git a/registry/gossip/watcher.go b/registry/gossip/watcher.go deleted file mode 100644 index 57b35521..00000000 --- a/registry/gossip/watcher.go +++ /dev/null @@ -1,53 +0,0 @@ -package gossip - -import ( - "github.com/micro/go-micro/registry" -) - -type gossipWatcher struct { - wo registry.WatchOptions - next chan *registry.Result - stop chan bool -} - -func newGossipWatcher(ch chan *registry.Result, stop chan bool, opts ...registry.WatchOption) (registry.Watcher, error) { - var wo registry.WatchOptions - for _, o := range opts { - o(&wo) - } - - return &gossipWatcher{ - wo: wo, - next: ch, - stop: stop, - }, nil -} - -func (m *gossipWatcher) Next() (*registry.Result, error) { - for { - select { - case r, ok := <-m.next: - if !ok { - return nil, registry.ErrWatcherStopped - } - // check watch options - if len(m.wo.Service) > 0 && r.Service.Name != m.wo.Service { - continue - } - nr := ®istry.Result{} - *nr = *r - return nr, nil - case <-m.stop: - return nil, registry.ErrWatcherStopped - } - } -} - -func (m *gossipWatcher) Stop() { - select { - case <-m.stop: - return - default: - close(m.stop) - } -} diff --git a/registry/handler/handler.go b/registry/handler/handler.go deleted file mode 100644 index f86148cd..00000000 --- a/registry/handler/handler.go +++ /dev/null @@ -1,76 +0,0 @@ -package handler - -import ( - "context" - - "github.com/micro/go-micro/errors" - "github.com/micro/go-micro/registry" - pb "github.com/micro/go-micro/registry/proto" - "github.com/micro/go-micro/registry/service" -) - -type Registry struct { - // internal registry - Registry registry.Registry -} - -func (r *Registry) GetService(ctx context.Context, req *pb.GetRequest, rsp *pb.GetResponse) error { - services, err := r.Registry.GetService(req.Service) - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - for _, srv := range services { - rsp.Services = append(rsp.Services, service.ToProto(srv)) - } - return nil -} - -func (r *Registry) Register(ctx context.Context, req *pb.Service, rsp *pb.EmptyResponse) error { - err := r.Registry.Register(service.ToService(req)) - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - return nil -} - -func (r *Registry) Deregister(ctx context.Context, req *pb.Service, rsp *pb.EmptyResponse) error { - err := r.Registry.Deregister(service.ToService(req)) - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - return nil -} - -func (r *Registry) ListServices(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error { - services, err := r.Registry.ListServices() - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - for _, srv := range services { - rsp.Services = append(rsp.Services, service.ToProto(srv)) - } - return nil -} - -func (r *Registry) Watch(ctx context.Context, req *pb.WatchRequest, rsp pb.Registry_WatchStream) error { - watcher, err := r.Registry.Watch(registry.WatchService(req.Service)) - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - - for { - next, err := watcher.Next() - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - err = rsp.Send(&pb.Result{ - Action: next.Action, - Service: service.ToProto(next.Service), - }) - if err != nil { - return errors.InternalServerError("go.micro.registry", err.Error()) - } - } - - return nil -} diff --git a/registry/mdns_registry.go b/registry/mdns_registry.go index 646032ad..fe952d22 100644 --- a/registry/mdns_registry.go +++ b/registry/mdns_registry.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/google/uuid" "github.com/micro/mdns" ) @@ -37,6 +38,14 @@ type mdnsRegistry struct { sync.Mutex services map[string][]*mdnsEntry + + mtx sync.RWMutex + + // watchers + watchers map[string]*mdnsWatcher + + // listener + listener chan *mdns.ServiceEntry } func newRegistry(opts ...Option) Registry { @@ -61,6 +70,7 @@ func newRegistry(opts ...Option) Registry { opts: options, domain: domain, services: make(map[string][]*mdnsEntry), + watchers: make(map[string]*mdnsWatcher), } } @@ -95,7 +105,7 @@ func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error return err } - srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{s}}) + srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{MDNSService: s}}) if err != nil { return err } @@ -220,7 +230,9 @@ func (m *mdnsRegistry) GetService(service string) ([]*Service, error) { p := mdns.DefaultParams(service) // set context with timeout - p.Context, _ = context.WithTimeout(context.Background(), m.opts.Timeout) + var cancel context.CancelFunc + p.Context, cancel = context.WithTimeout(context.Background(), m.opts.Timeout) + defer cancel() // set entries channel p.Entries = entries // set the domain @@ -282,7 +294,7 @@ func (m *mdnsRegistry) GetService(service string) ([]*Service, error) { <-done // create list and return - var services []*Service + services := make([]*Service, 0, len(serviceMap)) for _, service := range serviceMap { services = append(services, service) @@ -298,7 +310,9 @@ func (m *mdnsRegistry) ListServices() ([]*Service, error) { p := mdns.DefaultParams("_services") // set context with timeout - p.Context, _ = context.WithTimeout(context.Background(), m.opts.Timeout) + var cancel context.CancelFunc + p.Context, cancel = context.WithTimeout(context.Background(), m.opts.Timeout) + defer cancel() // set entries channel p.Entries = entries // set domain @@ -346,15 +360,88 @@ func (m *mdnsRegistry) Watch(opts ...WatchOption) (Watcher, error) { } md := &mdnsWatcher{ - wo: wo, - ch: make(chan *mdns.ServiceEntry, 32), - exit: make(chan struct{}), - domain: m.domain, + id: uuid.New().String(), + wo: wo, + ch: make(chan *mdns.ServiceEntry, 32), + exit: make(chan struct{}), + domain: m.domain, + registry: m, } + m.mtx.Lock() + defer m.mtx.Unlock() + + // save the watcher + m.watchers[md.id] = md + + // check of the listener exists + if m.listener != nil { + return md, nil + } + + // start the listener go func() { - if err := mdns.Listen(md.ch, md.exit); err != nil { - md.Stop() + // go to infinity + for { + m.mtx.Lock() + + // just return if there are no watchers + if len(m.watchers) == 0 { + m.listener = nil + m.mtx.Unlock() + return + } + + // check existing listener + if m.listener != nil { + m.mtx.Unlock() + return + } + + // reset the listener + exit := make(chan struct{}) + ch := make(chan *mdns.ServiceEntry, 32) + m.listener = ch + + m.mtx.Unlock() + + // send messages to the watchers + go func() { + send := func(w *mdnsWatcher, e *mdns.ServiceEntry) { + select { + case w.ch <- e: + default: + } + } + + for { + select { + case <-exit: + return + case e, ok := <-ch: + if !ok { + return + } + m.mtx.RLock() + // send service entry to all watchers + for _, w := range m.watchers { + send(w, e) + } + m.mtx.RUnlock() + } + } + + }() + + // start listening, blocking call + mdns.Listen(ch, exit) + + // mdns.Listen has unblocked + // kill the saved listener + m.mtx.Lock() + m.listener = nil + close(ch) + m.mtx.Unlock() } }() diff --git a/registry/mdns_test.go b/registry/mdns_test.go index e9813c69..9656d2dc 100644 --- a/registry/mdns_test.go +++ b/registry/mdns_test.go @@ -1,17 +1,18 @@ package registry import ( + "os" "testing" "time" ) func TestMDNS(t *testing.T) { testData := []*Service{ - &Service{ + { Name: "test1", Version: "1.0.1", Nodes: []*Node{ - &Node{ + { Id: "test1-1", Address: "10.0.0.1:10001", Metadata: map[string]string{ @@ -20,11 +21,11 @@ func TestMDNS(t *testing.T) { }, }, }, - &Service{ + { Name: "test2", Version: "1.0.2", Nodes: []*Node{ - &Node{ + { Id: "test2-1", Address: "10.0.0.2:10002", Metadata: map[string]string{ @@ -33,11 +34,11 @@ func TestMDNS(t *testing.T) { }, }, }, - &Service{ + { Name: "test3", Version: "1.0.3", Nodes: []*Node{ - &Node{ + { Id: "test3-1", Address: "10.0.0.3:10003", Metadata: map[string]string{ @@ -48,8 +49,16 @@ func TestMDNS(t *testing.T) { }, } + travis := os.Getenv("TRAVIS") + + var opts []Option + + if travis == "true" { + opts = append(opts, Timeout(time.Millisecond*100)) + } + // new registry - r := NewRegistry() + r := NewRegistry(opts...) for _, service := range testData { // register service diff --git a/registry/mdns_watcher.go b/registry/mdns_watcher.go index ce13866f..402811b9 100644 --- a/registry/mdns_watcher.go +++ b/registry/mdns_watcher.go @@ -8,11 +8,14 @@ import ( ) type mdnsWatcher struct { + id string wo WatchOptions ch chan *mdns.ServiceEntry exit chan struct{} // the mdns domain domain string + // the registry + registry *mdnsRegistry } func (m *mdnsWatcher) Next() (*Result, error) { @@ -76,5 +79,9 @@ func (m *mdnsWatcher) Stop() { return default: close(m.exit) + // remove self from the registry + m.registry.mtx.Lock() + delete(m.registry.watchers, m.id) + m.registry.mtx.Unlock() } } diff --git a/registry/memory/memory.go b/registry/memory/memory.go index 446aa0f0..3d7b782e 100644 --- a/registry/memory/memory.go +++ b/registry/memory/memory.go @@ -8,18 +8,34 @@ import ( "github.com/google/uuid" "github.com/micro/go-micro/registry" + "github.com/micro/go-micro/util/log" ) var ( - timeout = time.Millisecond * 10 + sendEventTime = 10 * time.Millisecond + ttlPruneTime = time.Second ) +type node struct { + *registry.Node + TTL time.Duration + LastSeen time.Time +} + +type record struct { + Name string + Version string + Metadata map[string]string + Nodes map[string]*node + Endpoints []*registry.Endpoint +} + type Registry struct { options registry.Options sync.RWMutex - Services map[string][]*registry.Service - Watchers map[string]*Watcher + records map[string]map[string]*record + watchers map[string]*Watcher } func NewRegistry(opts ...registry.Option) registry.Registry { @@ -31,23 +47,49 @@ func NewRegistry(opts ...registry.Option) registry.Registry { o(&options) } - services := getServices(options.Context) - if services == nil { - services = make(map[string][]*registry.Service) + records := getServiceRecords(options.Context) + if records == nil { + records = make(map[string]map[string]*record) } - return &Registry{ + reg := &Registry{ options: options, - Services: services, - Watchers: make(map[string]*Watcher), + records: records, + watchers: make(map[string]*Watcher), + } + + go reg.ttlPrune() + + return reg +} + +func (m *Registry) ttlPrune() { + prune := time.NewTicker(ttlPruneTime) + defer prune.Stop() + + for { + select { + case <-prune.C: + m.Lock() + for name, records := range m.records { + for version, record := range records { + for id, n := range record.Nodes { + if n.TTL != 0 && time.Since(n.LastSeen) > n.TTL { + log.Debugf("Registry TTL expired for node %s of service %s", n.Id, name) + delete(m.records[name][version].Nodes, id) + } + } + } + } + m.Unlock() + } } } func (m *Registry) sendEvent(r *registry.Result) { - var watchers []*Watcher - m.RLock() - for _, w := range m.Watchers { + watchers := make([]*Watcher, 0, len(m.watchers)) + for _, w := range m.watchers { watchers = append(watchers, w) } m.RUnlock() @@ -56,12 +98,12 @@ func (m *Registry) sendEvent(r *registry.Result) { select { case <-w.exit: m.Lock() - delete(m.Watchers, w.id) + delete(m.watchers, w.id) m.Unlock() default: select { case w.res <- r: - case <-time.After(timeout): + case <-time.After(sendEventTime): } } } @@ -74,11 +116,24 @@ func (m *Registry) Init(opts ...registry.Option) error { // add services m.Lock() - for k, v := range getServices(m.options.Context) { - s := m.Services[k] - m.Services[k] = registry.Merge(s, v) + defer m.Unlock() + + records := getServiceRecords(m.options.Context) + for name, record := range records { + // add a whole new service including all of its versions + if _, ok := m.records[name]; !ok { + m.records[name] = record + continue + } + // add the versions of the service we dont track yet + for version, r := range record { + if _, ok := m.records[name][version]; !ok { + m.records[name][version] = r + continue + } + } } - m.Unlock() + return nil } @@ -86,62 +141,61 @@ func (m *Registry) Options() registry.Options { return m.options } -func (m *Registry) GetService(name string) ([]*registry.Service, error) { - m.RLock() - service, ok := m.Services[name] - m.RUnlock() - if !ok { - return nil, registry.ErrNotFound - } - - return service, nil -} - -func (m *Registry) ListServices() ([]*registry.Service, error) { - var services []*registry.Service - m.RLock() - 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 { m.Lock() defer m.Unlock() - if service, ok := m.Services[s.Name]; !ok { - m.Services[s.Name] = []*registry.Service{s} + var options registry.RegisterOptions + for _, o := range opts { + o(&options) + } + + r := serviceToRecord(s, options.TTL) + + if _, ok := m.records[s.Name]; !ok { + m.records[s.Name] = make(map[string]*record) + } + + if _, ok := m.records[s.Name][s.Version]; !ok { + m.records[s.Name][s.Version] = r + log.Debugf("Registry added new service: %s, version: %s", s.Name, s.Version) go m.sendEvent(®istry.Result{Action: "update", Service: s}) - } else { - svcCount := len(service) - svcNodeCounts := make(map[string]map[string]int) - for _, s := range service { - if _, ok := svcNodeCounts[s.Name]; !ok { - svcNodeCounts[s.Name] = make(map[string]int) - } - if _, ok := svcNodeCounts[s.Name][s.Version]; !ok { - svcNodeCounts[s.Name][s.Version] = len(s.Nodes) - } - } - // if merged count and original service counts changed we added new version of the service - merged := registry.Merge(service, []*registry.Service{s}) - if len(merged) != svcCount { - m.Services[s.Name] = merged - go m.sendEvent(®istry.Result{Action: "update", Service: s}) - return nil - } - // if the node count for a particular service has changed we added a new node to the service - for _, s := range merged { - if len(s.Nodes) != svcNodeCounts[s.Name][s.Version] { - m.Services[s.Name] = merged - go m.sendEvent(®istry.Result{Action: "update", Service: s}) - return nil + return nil + } + + addedNodes := false + for _, n := range s.Nodes { + if _, ok := m.records[s.Name][s.Version].Nodes[n.Id]; !ok { + addedNodes = true + metadata := make(map[string]string) + for k, v := range n.Metadata { + metadata[k] = v + m.records[s.Name][s.Version].Nodes[n.Id] = &node{ + Node: ®istry.Node{ + Id: n.Id, + Address: n.Address, + Metadata: metadata, + }, + TTL: options.TTL, + LastSeen: time.Now(), + } } } } + if addedNodes { + log.Debugf("Registry added new node to service: %s, version: %s", s.Name, s.Version) + go m.sendEvent(®istry.Result{Action: "update", Service: s}) + return nil + } + + // refresh TTL and timestamp + for _, n := range s.Nodes { + log.Debugf("Updated registration for service: %s, version: %s", s.Name, s.Version) + m.records[s.Name][s.Version].Nodes[n.Id].TTL = options.TTL + m.records[s.Name][s.Version].Nodes[n.Id].LastSeen = time.Now() + } + return nil } @@ -149,18 +203,62 @@ func (m *Registry) Deregister(s *registry.Service) error { m.Lock() defer m.Unlock() - if service, ok := m.Services[s.Name]; ok { - go m.sendEvent(®istry.Result{Action: "delete", Service: s}) - if service := registry.Remove(service, []*registry.Service{s}); len(service) == 0 { - delete(m.Services, s.Name) - } else { - m.Services[s.Name] = service + if _, ok := m.records[s.Name]; ok { + if _, ok := m.records[s.Name][s.Version]; ok { + for _, n := range s.Nodes { + if _, ok := m.records[s.Name][s.Version].Nodes[n.Id]; ok { + log.Debugf("Registry removed node from service: %s, version: %s", s.Name, s.Version) + delete(m.records[s.Name][s.Version].Nodes, n.Id) + } + } + if len(m.records[s.Name][s.Version].Nodes) == 0 { + delete(m.records[s.Name], s.Version) + log.Debugf("Registry removed service: %s, version: %s", s.Name, s.Version) + } } + if len(m.records[s.Name]) == 0 { + delete(m.records, s.Name) + log.Debugf("Registry removed service: %s", s.Name) + } + go m.sendEvent(®istry.Result{Action: "delete", Service: s}) } return nil } +func (m *Registry) GetService(name string) ([]*registry.Service, error) { + m.RLock() + defer m.RUnlock() + + records, ok := m.records[name] + if !ok { + return nil, registry.ErrNotFound + } + + services := make([]*registry.Service, len(m.records[name])) + i := 0 + for _, record := range records { + services[i] = recordToService(record) + i++ + } + + return services, nil +} + +func (m *Registry) ListServices() ([]*registry.Service, error) { + m.RLock() + defer m.RUnlock() + + var services []*registry.Service + for _, records := range m.records { + for _, record := range records { + services = append(services, recordToService(record)) + } + } + + return services, nil +} + func (m *Registry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { var wo registry.WatchOptions for _, o := range opts { @@ -175,8 +273,9 @@ func (m *Registry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) } m.Lock() - m.Watchers[w.id] = w + m.watchers[w.id] = w m.Unlock() + return w, nil } diff --git a/registry/memory/memory_test.go b/registry/memory/memory_test.go index fa8b53f1..cab6a6f4 100644 --- a/registry/memory/memory_test.go +++ b/registry/memory/memory_test.go @@ -1,14 +1,16 @@ package memory import ( + "fmt" "testing" + "time" "github.com/micro/go-micro/registry" ) var ( testData = map[string][]*registry.Service{ - "foo": []*registry.Service{ + "foo": { { Name: "foo", Version: "1.0.0", @@ -44,7 +46,7 @@ var ( }, }, }, - "bar": []*registry.Service{ + "bar": { { Name: "bar", Version: "default", @@ -102,10 +104,20 @@ func TestMemoryRegistry(t *testing.T) { // register data for _, v := range testData { + serviceCount := 0 for _, service := range v { if err := m.Register(service); err != nil { t.Errorf("Unexpected register error: %v", err) } + serviceCount++ + // after the service has been registered we should be able to query it + services, err := m.GetService(service.Name) + if err != nil { + t.Errorf("Unexpected error getting service %s: %v", service.Name, err) + } + if len(services) != serviceCount { + t.Errorf("Expected %d services for %s, got %d", serviceCount, service.Name, len(services)) + } } } @@ -114,6 +126,22 @@ func TestMemoryRegistry(t *testing.T) { fn(k, v) } + services, err := m.ListServices() + if err != nil { + t.Errorf("Unexpected error when listing services: %v", err) + } + + totalServiceCount := 0 + for _, testSvc := range testData { + for range testSvc { + totalServiceCount++ + } + } + + if len(services) != totalServiceCount { + t.Errorf("Expected total service count: %d, got: %d", totalServiceCount, len(services)) + } + // deregister for _, v := range testData { for _, service := range v { @@ -122,4 +150,94 @@ func TestMemoryRegistry(t *testing.T) { } } } + + // after all the service nodes have been deregistered we should not get any results + for _, v := range testData { + for _, service := range v { + services, err := m.GetService(service.Name) + if err != registry.ErrNotFound { + t.Errorf("Expected error: %v, got: %v", registry.ErrNotFound, err) + } + if len(services) != 0 { + t.Errorf("Expected %d services for %s, got %d", 0, service.Name, len(services)) + } + } + } +} + +func TestMemoryRegistryTTL(t *testing.T) { + m := NewRegistry() + + for _, v := range testData { + for _, service := range v { + if err := m.Register(service, registry.RegisterTTL(time.Millisecond)); err != nil { + t.Fatal(err) + } + } + } + + time.Sleep(ttlPruneTime * 2) + + for name := range testData { + svcs, err := m.GetService(name) + if err != nil { + t.Fatal(err) + } + + for _, svc := range svcs { + if len(svc.Nodes) > 0 { + t.Fatalf("Service %q still has nodes registered", name) + } + } + } +} + +func TestMemoryRegistryTTLConcurrent(t *testing.T) { + concurrency := 1000 + waitTime := ttlPruneTime * 2 + m := NewRegistry() + + for _, v := range testData { + for _, service := range v { + if err := m.Register(service, registry.RegisterTTL(waitTime/2)); err != nil { + t.Fatal(err) + } + } + } + + t.Logf("test will wait %v, then check TTL timeouts", waitTime) + + errChan := make(chan error, concurrency) + syncChan := make(chan struct{}) + + for i := 0; i < concurrency; i++ { + go func() { + <-syncChan + for name := range testData { + svcs, err := m.GetService(name) + if err != nil { + errChan <- err + return + } + + for _, svc := range svcs { + if len(svc.Nodes) > 0 { + errChan <- fmt.Errorf("Service %q still has nodes registered", name) + return + } + } + } + + errChan <- nil + }() + } + + time.Sleep(waitTime) + close(syncChan) + + for i := 0; i < concurrency; i++ { + if err := <-errChan; err != nil { + t.Fatal(err) + } + } } diff --git a/registry/memory/memory_watcher.go b/registry/memory/memory_watcher.go index 3ec2071b..8eb3cfd1 100644 --- a/registry/memory/memory_watcher.go +++ b/registry/memory/memory_watcher.go @@ -13,10 +13,8 @@ type memoryWatcher struct { func (m *memoryWatcher) Next() (*registry.Result, error) { // not implement so we just block until exit - select { - case <-m.exit: - return nil, errors.New("watcher stopped") - } + <-m.exit + return nil, errors.New("watcher stopped") } func (m *memoryWatcher) Stop() { diff --git a/registry/memory/options.go b/registry/memory/options.go index 64680fdc..7922dcaa 100644 --- a/registry/memory/options.go +++ b/registry/memory/options.go @@ -8,12 +8,25 @@ import ( type servicesKey struct{} -func getServices(ctx context.Context) map[string][]*registry.Service { - s, ok := ctx.Value(servicesKey{}).(map[string][]*registry.Service) +func getServiceRecords(ctx context.Context) map[string]map[string]*record { + memServices, ok := ctx.Value(servicesKey{}).(map[string][]*registry.Service) if !ok { return nil } - return s + + services := make(map[string]map[string]*record) + + for name, svc := range memServices { + if _, ok := services[name]; !ok { + services[name] = make(map[string]*record) + } + // go through every version of the service + for _, s := range svc { + services[s.Name][s.Version] = serviceToRecord(s, 0) + } + } + + return services } // Services is an option that preloads service data diff --git a/registry/memory/util.go b/registry/memory/util.go new file mode 100644 index 00000000..d78e3324 --- /dev/null +++ b/registry/memory/util.go @@ -0,0 +1,91 @@ +package memory + +import ( + "time" + + "github.com/micro/go-micro/registry" +) + +func serviceToRecord(s *registry.Service, ttl time.Duration) *record { + metadata := make(map[string]string) + for k, v := range s.Metadata { + metadata[k] = v + } + + nodes := make(map[string]*node) + for _, n := range s.Nodes { + nodes[n.Id] = &node{ + Node: n, + TTL: ttl, + LastSeen: time.Now(), + } + } + + endpoints := make([]*registry.Endpoint, len(s.Endpoints)) + for i, e := range s.Endpoints { + endpoints[i] = e + } + + return &record{ + Name: s.Name, + Version: s.Version, + Metadata: metadata, + Nodes: nodes, + Endpoints: endpoints, + } +} + +func recordToService(r *record) *registry.Service { + metadata := make(map[string]string) + for k, v := range r.Metadata { + metadata[k] = v + } + + endpoints := make([]*registry.Endpoint, len(r.Endpoints)) + for i, e := range r.Endpoints { + request := new(registry.Value) + if e.Request != nil { + *request = *e.Request + } + response := new(registry.Value) + if e.Response != nil { + *response = *e.Response + } + + metadata := make(map[string]string) + for k, v := range e.Metadata { + metadata[k] = v + } + + endpoints[i] = ®istry.Endpoint{ + Name: e.Name, + Request: request, + Response: response, + Metadata: metadata, + } + } + + nodes := make([]*registry.Node, len(r.Nodes)) + i := 0 + for _, n := range r.Nodes { + metadata := make(map[string]string) + for k, v := range n.Metadata { + metadata[k] = v + } + + nodes[i] = ®istry.Node{ + Id: n.Id, + Address: n.Address, + Metadata: metadata, + } + i++ + } + + return ®istry.Service{ + Name: r.Name, + Version: r.Version, + Metadata: metadata, + Endpoints: endpoints, + Nodes: nodes, + } +} diff --git a/registry/proto/registry.pb.go b/registry/service/proto/registry.pb.go similarity index 75% rename from registry/proto/registry.pb.go rename to registry/service/proto/registry.pb.go index 9c62a1fb..9383e99a 100644 --- a/registry/proto/registry.pb.go +++ b/registry/service/proto/registry.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: registry.proto +// source: micro/go-micro/registry/service/proto/registry.proto package go_micro_registry @@ -46,7 +46,7 @@ func (x EventType) String() string { } func (EventType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{0} + return fileDescriptor_2f73432195c6499a, []int{0} } // Service represents a go-micro service @@ -56,6 +56,7 @@ type Service struct { Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Endpoints []*Endpoint `protobuf:"bytes,4,rep,name=endpoints,proto3" json:"endpoints,omitempty"` Nodes []*Node `protobuf:"bytes,5,rep,name=nodes,proto3" json:"nodes,omitempty"` + Options *Options `protobuf:"bytes,6,opt,name=options,proto3" json:"options,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -65,7 +66,7 @@ func (m *Service) Reset() { *m = Service{} } func (m *Service) String() string { return proto.CompactTextString(m) } func (*Service) ProtoMessage() {} func (*Service) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{0} + return fileDescriptor_2f73432195c6499a, []int{0} } func (m *Service) XXX_Unmarshal(b []byte) error { @@ -121,6 +122,13 @@ func (m *Service) GetNodes() []*Node { return nil } +func (m *Service) GetOptions() *Options { + if m != nil { + return m.Options + } + return nil +} + // Node represents the node the service is on type Node struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -136,7 +144,7 @@ func (m *Node) Reset() { *m = Node{} } func (m *Node) String() string { return proto.CompactTextString(m) } func (*Node) ProtoMessage() {} func (*Node) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{1} + return fileDescriptor_2f73432195c6499a, []int{1} } func (m *Node) XXX_Unmarshal(b []byte) error { @@ -200,7 +208,7 @@ func (m *Endpoint) Reset() { *m = Endpoint{} } func (m *Endpoint) String() string { return proto.CompactTextString(m) } func (*Endpoint) ProtoMessage() {} func (*Endpoint) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{2} + return fileDescriptor_2f73432195c6499a, []int{2} } func (m *Endpoint) XXX_Unmarshal(b []byte) error { @@ -263,7 +271,7 @@ func (m *Value) Reset() { *m = Value{} } func (m *Value) String() string { return proto.CompactTextString(m) } func (*Value) ProtoMessage() {} func (*Value) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{3} + return fileDescriptor_2f73432195c6499a, []int{3} } func (m *Value) XXX_Unmarshal(b []byte) error { @@ -305,6 +313,46 @@ func (m *Value) GetValues() []*Value { return nil } +// Options are registry options +type Options struct { + Ttl int64 `protobuf:"varint,1,opt,name=ttl,proto3" json:"ttl,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Options) Reset() { *m = Options{} } +func (m *Options) String() string { return proto.CompactTextString(m) } +func (*Options) ProtoMessage() {} +func (*Options) Descriptor() ([]byte, []int) { + return fileDescriptor_2f73432195c6499a, []int{4} +} + +func (m *Options) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Options.Unmarshal(m, b) +} +func (m *Options) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Options.Marshal(b, m, deterministic) +} +func (m *Options) XXX_Merge(src proto.Message) { + xxx_messageInfo_Options.Merge(m, src) +} +func (m *Options) XXX_Size() int { + return xxx_messageInfo_Options.Size(m) +} +func (m *Options) XXX_DiscardUnknown() { + xxx_messageInfo_Options.DiscardUnknown(m) +} + +var xxx_messageInfo_Options proto.InternalMessageInfo + +func (m *Options) GetTtl() int64 { + if m != nil { + return m.Ttl + } + return 0 +} + // Result is returns by the watcher type Result struct { Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` @@ -319,7 +367,7 @@ func (m *Result) Reset() { *m = Result{} } func (m *Result) String() string { return proto.CompactTextString(m) } func (*Result) ProtoMessage() {} func (*Result) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{4} + return fileDescriptor_2f73432195c6499a, []int{5} } func (m *Result) XXX_Unmarshal(b []byte) error { @@ -371,7 +419,7 @@ func (m *EmptyResponse) Reset() { *m = EmptyResponse{} } func (m *EmptyResponse) String() string { return proto.CompactTextString(m) } func (*EmptyResponse) ProtoMessage() {} func (*EmptyResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{5} + return fileDescriptor_2f73432195c6499a, []int{6} } func (m *EmptyResponse) XXX_Unmarshal(b []byte) error { @@ -403,7 +451,7 @@ func (m *GetRequest) Reset() { *m = GetRequest{} } func (m *GetRequest) String() string { return proto.CompactTextString(m) } func (*GetRequest) ProtoMessage() {} func (*GetRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{6} + return fileDescriptor_2f73432195c6499a, []int{7} } func (m *GetRequest) XXX_Unmarshal(b []byte) error { @@ -442,7 +490,7 @@ func (m *GetResponse) Reset() { *m = GetResponse{} } func (m *GetResponse) String() string { return proto.CompactTextString(m) } func (*GetResponse) ProtoMessage() {} func (*GetResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{7} + return fileDescriptor_2f73432195c6499a, []int{8} } func (m *GetResponse) XXX_Unmarshal(b []byte) error { @@ -480,7 +528,7 @@ func (m *ListRequest) Reset() { *m = ListRequest{} } func (m *ListRequest) String() string { return proto.CompactTextString(m) } func (*ListRequest) ProtoMessage() {} func (*ListRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{8} + return fileDescriptor_2f73432195c6499a, []int{9} } func (m *ListRequest) XXX_Unmarshal(b []byte) error { @@ -512,7 +560,7 @@ func (m *ListResponse) Reset() { *m = ListResponse{} } func (m *ListResponse) String() string { return proto.CompactTextString(m) } func (*ListResponse) ProtoMessage() {} func (*ListResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{9} + return fileDescriptor_2f73432195c6499a, []int{10} } func (m *ListResponse) XXX_Unmarshal(b []byte) error { @@ -552,7 +600,7 @@ func (m *WatchRequest) Reset() { *m = WatchRequest{} } func (m *WatchRequest) String() string { return proto.CompactTextString(m) } func (*WatchRequest) ProtoMessage() {} func (*WatchRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{10} + return fileDescriptor_2f73432195c6499a, []int{11} } func (m *WatchRequest) XXX_Unmarshal(b []byte) error { @@ -599,7 +647,7 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_41af05d40a615591, []int{11} + return fileDescriptor_2f73432195c6499a, []int{12} } func (m *Event) XXX_Unmarshal(b []byte) error { @@ -657,6 +705,7 @@ func init() { proto.RegisterType((*Endpoint)(nil), "go.micro.registry.Endpoint") proto.RegisterMapType((map[string]string)(nil), "go.micro.registry.Endpoint.MetadataEntry") proto.RegisterType((*Value)(nil), "go.micro.registry.Value") + proto.RegisterType((*Options)(nil), "go.micro.registry.Options") proto.RegisterType((*Result)(nil), "go.micro.registry.Result") proto.RegisterType((*EmptyResponse)(nil), "go.micro.registry.EmptyResponse") proto.RegisterType((*GetRequest)(nil), "go.micro.registry.GetRequest") @@ -667,48 +716,53 @@ func init() { proto.RegisterType((*Event)(nil), "go.micro.registry.Event") } -func init() { proto.RegisterFile("registry.proto", fileDescriptor_41af05d40a615591) } - -var fileDescriptor_41af05d40a615591 = []byte{ - // 632 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdb, 0x6e, 0xd3, 0x4c, - 0x10, 0x8e, 0xed, 0x38, 0x87, 0x49, 0xdb, 0xbf, 0xff, 0x08, 0x81, 0x31, 0x05, 0x22, 0x4b, 0xa0, - 0x80, 0x84, 0xa9, 0x42, 0x85, 0x38, 0x5c, 0x21, 0x62, 0x2a, 0xa1, 0x16, 0x09, 0x73, 0xba, 0x36, - 0xf1, 0xa8, 0x58, 0xc4, 0x07, 0x76, 0x37, 0x91, 0xfc, 0x0e, 0x48, 0x3c, 0x01, 0x77, 0x3c, 0x0a, - 0x0f, 0x86, 0xbc, 0x5e, 0x27, 0xa9, 0x62, 0x07, 0xa4, 0xc2, 0xdd, 0x8c, 0xf7, 0x9b, 0x6f, 0x67, - 0xbe, 0x6f, 0x36, 0x81, 0x3d, 0x46, 0x67, 0x11, 0x17, 0x2c, 0x77, 0x33, 0x96, 0x8a, 0x14, 0xff, - 0x3f, 0x4b, 0xdd, 0x38, 0x9a, 0xb2, 0xd4, 0xad, 0x0e, 0x9c, 0x1f, 0x3a, 0x74, 0xdf, 0x10, 0x5b, - 0x44, 0x53, 0x42, 0x84, 0x76, 0x12, 0xc4, 0x64, 0x69, 0x43, 0x6d, 0xd4, 0xf7, 0x65, 0x8c, 0x16, - 0x74, 0x17, 0xc4, 0x78, 0x94, 0x26, 0x96, 0x2e, 0x3f, 0x57, 0x29, 0x4e, 0xa0, 0x17, 0x93, 0x08, - 0xc2, 0x40, 0x04, 0x96, 0x31, 0x34, 0x46, 0x83, 0xf1, 0xc8, 0xdd, 0xe0, 0x77, 0x15, 0xb7, 0x7b, - 0xaa, 0xa0, 0x5e, 0x22, 0x58, 0xee, 0x2f, 0x2b, 0xf1, 0x31, 0xf4, 0x29, 0x09, 0xb3, 0x34, 0x4a, - 0x04, 0xb7, 0xda, 0x92, 0xe6, 0x5a, 0x0d, 0x8d, 0xa7, 0x30, 0xfe, 0x0a, 0x8d, 0xf7, 0xc0, 0x4c, - 0xd2, 0x90, 0xb8, 0x65, 0xca, 0xb2, 0x2b, 0x35, 0x65, 0xaf, 0xd2, 0x90, 0xfc, 0x12, 0x65, 0x3f, - 0x85, 0xdd, 0x73, 0x4d, 0xe0, 0x3e, 0x18, 0x9f, 0x29, 0x57, 0xd3, 0x16, 0x21, 0x5e, 0x02, 0x73, - 0x11, 0xcc, 0xe6, 0xa4, 0x46, 0x2d, 0x93, 0x27, 0xfa, 0x23, 0xcd, 0xf9, 0xa9, 0x41, 0xbb, 0x20, - 0xc3, 0x3d, 0xd0, 0xa3, 0x50, 0xd5, 0xe8, 0x51, 0x58, 0xe8, 0x13, 0x84, 0x21, 0x23, 0xce, 0x2b, - 0x7d, 0x54, 0x5a, 0xa8, 0x99, 0xa5, 0x4c, 0x58, 0xc6, 0x50, 0x1b, 0x19, 0xbe, 0x8c, 0xf1, 0xd9, - 0x9a, 0x66, 0xe5, 0xb0, 0xb7, 0x1a, 0xba, 0x6e, 0x12, 0xec, 0x62, 0x63, 0x7c, 0xd5, 0xa1, 0x57, - 0x49, 0x59, 0x6b, 0xf7, 0x18, 0xba, 0x8c, 0xbe, 0xcc, 0x89, 0x0b, 0x59, 0x3c, 0x18, 0x5b, 0x35, - 0xfd, 0xbd, 0x2f, 0xf8, 0xfc, 0x0a, 0x88, 0x47, 0xd0, 0x63, 0xc4, 0xb3, 0x34, 0xe1, 0x24, 0x87, - 0xdd, 0x56, 0xb4, 0x44, 0xa2, 0xb7, 0x21, 0xc5, 0x9d, 0x2d, 0xbe, 0xff, 0x1b, 0x39, 0x02, 0x30, - 0x65, 0x5b, 0xb5, 0x52, 0x20, 0xb4, 0x45, 0x9e, 0x55, 0x55, 0x32, 0xc6, 0x43, 0xe8, 0xc8, 0x6a, - 0xae, 0x36, 0xbe, 0x79, 0x50, 0x85, 0x73, 0x04, 0x74, 0x7c, 0xe2, 0xf3, 0x99, 0xc0, 0xcb, 0xd0, - 0x09, 0xa6, 0xa2, 0x78, 0x48, 0xe5, 0x2d, 0x2a, 0xc3, 0x23, 0xe8, 0xf2, 0xf2, 0x91, 0x28, 0xc9, - 0xed, 0xe6, 0x67, 0xe4, 0x57, 0x50, 0x3c, 0x80, 0xbe, 0x88, 0x62, 0xe2, 0x22, 0x88, 0x33, 0xb5, - 0x62, 0xab, 0x0f, 0xce, 0x7f, 0xb0, 0xeb, 0xc5, 0x99, 0xc8, 0x7d, 0xa5, 0xb6, 0x73, 0x1b, 0xe0, - 0x98, 0x84, 0xaf, 0x1c, 0xb3, 0x56, 0x57, 0x96, 0xbd, 0x54, 0xa9, 0xe3, 0xc1, 0x40, 0xe2, 0x94, - 0x49, 0x0f, 0xa1, 0xa7, 0x4e, 0xb8, 0xa5, 0xc9, 0x89, 0xb7, 0x35, 0xb7, 0xc4, 0x3a, 0xbb, 0x30, - 0x38, 0x89, 0x78, 0x75, 0x9f, 0xf3, 0x02, 0x76, 0xca, 0xf4, 0x82, 0xb4, 0x23, 0xd8, 0xf9, 0x10, - 0x88, 0xe9, 0xa7, 0xdf, 0xcf, 0xf1, 0x5d, 0x03, 0xd3, 0x5b, 0x50, 0x22, 0x36, 0x1e, 0xec, 0xe1, - 0x9a, 0xad, 0x7b, 0xe3, 0x83, 0xba, 0x9d, 0x2b, 0xea, 0xde, 0xe6, 0x19, 0x29, 0xd3, 0xb7, 0x4a, - 0xbd, 0x6e, 0x5f, 0xfb, 0x8f, 0xed, 0xbb, 0x7b, 0x1f, 0xfa, 0xcb, 0x6b, 0x10, 0xa0, 0xf3, 0x9c, - 0x51, 0x20, 0x68, 0xbf, 0x55, 0xc4, 0x13, 0x9a, 0x91, 0xa0, 0x7d, 0xad, 0x88, 0xdf, 0x65, 0x61, - 0xf1, 0x5d, 0x1f, 0x7f, 0x33, 0xa0, 0xe7, 0x2b, 0x3a, 0x3c, 0x95, 0x6e, 0x56, 0x3f, 0xdb, 0xd7, - 0x6b, 0x2e, 0x5c, 0x99, 0x6d, 0xdf, 0x68, 0x3a, 0x56, 0xab, 0xd1, 0xc2, 0x97, 0x15, 0x35, 0x31, - 0xdc, 0xd2, 0xbd, 0x3d, 0xac, 0x13, 0xeb, 0xdc, 0x9a, 0xb5, 0xf0, 0x04, 0x60, 0x42, 0xec, 0x6f, - 0xb1, 0xbd, 0x2e, 0x17, 0x47, 0x95, 0x70, 0xac, 0x9b, 0x65, 0x6d, 0xd1, 0xec, 0x9b, 0x8d, 0xe7, - 0x4b, 0xca, 0x63, 0x30, 0xe5, 0x0e, 0x61, 0x1d, 0x76, 0x7d, 0xbb, 0xec, 0xab, 0x35, 0x80, 0xf2, - 0x2d, 0x3b, 0xad, 0x43, 0xed, 0x63, 0x47, 0xfe, 0xa7, 0x3e, 0xf8, 0x15, 0x00, 0x00, 0xff, 0xff, - 0x08, 0x0e, 0xe4, 0xae, 0x65, 0x07, 0x00, 0x00, +func init() { + proto.RegisterFile("micro/go-micro/registry/service/proto/registry.proto", fileDescriptor_2f73432195c6499a) +} + +var fileDescriptor_2f73432195c6499a = []byte{ + // 681 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdd, 0x6e, 0xd3, 0x4c, + 0x10, 0x8d, 0xed, 0xfc, 0x4e, 0xda, 0x7e, 0xfd, 0x46, 0x08, 0x8c, 0x5b, 0x20, 0xb2, 0x04, 0x0a, + 0x48, 0x4d, 0xaa, 0x50, 0x21, 0x7e, 0xae, 0x10, 0x0d, 0x95, 0x50, 0x0b, 0x62, 0xf9, 0xbb, 0x36, + 0xf1, 0xa8, 0x58, 0x24, 0xb6, 0xd9, 0xdd, 0x46, 0xca, 0x3b, 0x20, 0xf1, 0x04, 0xbc, 0x0d, 0x4f, + 0xc1, 0xd3, 0xa0, 0x5d, 0xaf, 0x93, 0x54, 0xdd, 0x04, 0xa4, 0xc2, 0xdd, 0xcc, 0xee, 0x39, 0xb3, + 0xb3, 0x67, 0xce, 0xda, 0x70, 0x30, 0x49, 0x46, 0x3c, 0xeb, 0x9f, 0x66, 0x7b, 0x45, 0xc0, 0xe9, + 0x34, 0x11, 0x92, 0xcf, 0xfa, 0x82, 0xf8, 0x34, 0x19, 0x51, 0x3f, 0xe7, 0x99, 0x5c, 0x2c, 0xf7, + 0x74, 0x8a, 0xff, 0x9f, 0x66, 0x3d, 0x8d, 0xef, 0x95, 0x1b, 0xe1, 0x4f, 0x17, 0x1a, 0x6f, 0x0a, + 0x0e, 0x22, 0x54, 0xd3, 0x68, 0x42, 0xbe, 0xd3, 0x71, 0xba, 0x2d, 0xa6, 0x63, 0xf4, 0xa1, 0x31, + 0x25, 0x2e, 0x92, 0x2c, 0xf5, 0x5d, 0xbd, 0x5c, 0xa6, 0x78, 0x08, 0xcd, 0x09, 0xc9, 0x28, 0x8e, + 0x64, 0xe4, 0x7b, 0x1d, 0xaf, 0xdb, 0x1e, 0x74, 0x7b, 0x17, 0xea, 0xf7, 0x4c, 0xed, 0xde, 0x89, + 0x81, 0x0e, 0x53, 0xc9, 0x67, 0x6c, 0xce, 0xc4, 0x47, 0xd0, 0xa2, 0x34, 0xce, 0xb3, 0x24, 0x95, + 0xc2, 0xaf, 0xea, 0x32, 0x3b, 0x96, 0x32, 0x43, 0x83, 0x61, 0x0b, 0x34, 0xee, 0x41, 0x2d, 0xcd, + 0x62, 0x12, 0x7e, 0x4d, 0xd3, 0xae, 0x59, 0x68, 0x2f, 0xb3, 0x98, 0x58, 0x81, 0xc2, 0x03, 0x68, + 0x64, 0xb9, 0x4c, 0xb2, 0x54, 0xf8, 0xf5, 0x8e, 0xd3, 0x6d, 0x0f, 0x02, 0x0b, 0xe1, 0x55, 0x81, + 0x60, 0x25, 0x34, 0x78, 0x02, 0x9b, 0xe7, 0x5a, 0xc7, 0x6d, 0xf0, 0x3e, 0xd3, 0xcc, 0x68, 0xa4, + 0x42, 0xbc, 0x02, 0xb5, 0x69, 0x34, 0x3e, 0x23, 0x23, 0x50, 0x91, 0x3c, 0x76, 0x1f, 0x3a, 0xe1, + 0x0f, 0x07, 0xaa, 0xaa, 0x05, 0xdc, 0x02, 0x37, 0x89, 0x0d, 0xc7, 0x4d, 0x62, 0xa5, 0x6a, 0x14, + 0xc7, 0x9c, 0x84, 0x28, 0x55, 0x35, 0xa9, 0x9a, 0x41, 0x9e, 0x71, 0xe9, 0x7b, 0x1d, 0xa7, 0xeb, + 0x31, 0x1d, 0xe3, 0xd3, 0x25, 0xa5, 0x0b, 0x89, 0x6e, 0xaf, 0xb8, 0xeb, 0x2a, 0x99, 0x2f, 0x77, + 0x8d, 0xaf, 0x2e, 0x34, 0xcb, 0x01, 0x58, 0x4d, 0x32, 0x80, 0x06, 0xa7, 0x2f, 0x67, 0x24, 0xa4, + 0x26, 0xb7, 0x07, 0xbe, 0xa5, 0xbf, 0xf7, 0xaa, 0x1e, 0x2b, 0x81, 0x78, 0x00, 0x4d, 0x4e, 0x22, + 0xcf, 0x52, 0x41, 0xfa, 0xb2, 0xeb, 0x48, 0x73, 0x24, 0x0e, 0x2f, 0x48, 0x71, 0x77, 0x8d, 0x5b, + 0xfe, 0x8d, 0x1c, 0x11, 0xd4, 0x74, 0x5b, 0x56, 0x29, 0x10, 0xaa, 0x72, 0x96, 0x97, 0x2c, 0x1d, + 0xe3, 0x3e, 0xd4, 0x35, 0x5b, 0x98, 0x77, 0xb2, 0xfa, 0xa2, 0x06, 0x17, 0xee, 0x40, 0xc3, 0x38, + 0x51, 0x75, 0x26, 0xe5, 0x58, 0x9f, 0xe1, 0x31, 0x15, 0x86, 0x12, 0xea, 0x8c, 0xc4, 0xd9, 0x58, + 0xe2, 0x55, 0xa8, 0x47, 0x23, 0x05, 0x33, 0x2d, 0x98, 0x4c, 0x59, 0xdd, 0x7c, 0x07, 0xcc, 0x3c, + 0x82, 0xd5, 0x2f, 0x93, 0x95, 0x50, 0xdc, 0x85, 0x96, 0x4c, 0x26, 0x24, 0x64, 0x34, 0xc9, 0x8d, + 0xff, 0x16, 0x0b, 0xe1, 0x7f, 0xb0, 0x39, 0x9c, 0xe4, 0x72, 0xc6, 0xcc, 0x28, 0xc2, 0x3b, 0x00, + 0x47, 0x24, 0x99, 0x19, 0xa7, 0xbf, 0x38, 0xb2, 0xe8, 0xa5, 0x4c, 0xc3, 0x21, 0xb4, 0x35, 0xce, + 0x4c, 0xf0, 0x01, 0x34, 0xcd, 0x8e, 0xf0, 0x1d, 0x2d, 0xc7, 0xba, 0xe6, 0xe6, 0xd8, 0x70, 0x13, + 0xda, 0xc7, 0x89, 0x28, 0xcf, 0x0b, 0x9f, 0xc3, 0x46, 0x91, 0x5e, 0xb2, 0x6c, 0x17, 0x36, 0x3e, + 0x44, 0x72, 0xf4, 0xe9, 0xf7, 0xf7, 0xf8, 0xee, 0x40, 0x6d, 0x38, 0xa5, 0x54, 0x5e, 0x78, 0xcd, + 0xfb, 0x4b, 0x33, 0xdf, 0x1a, 0xec, 0xda, 0x0c, 0xa9, 0x78, 0x6f, 0x67, 0x39, 0x19, 0x47, 0xac, + 0x95, 0x7a, 0x79, 0x7c, 0xd5, 0x3f, 0x1e, 0xdf, 0xbd, 0x3e, 0xb4, 0xe6, 0xc7, 0x20, 0x40, 0xfd, + 0x19, 0xa7, 0x48, 0xd2, 0x76, 0x45, 0xc5, 0x87, 0x34, 0x26, 0x49, 0xdb, 0x8e, 0x8a, 0xdf, 0xe5, + 0xb1, 0x5a, 0x77, 0x07, 0xdf, 0x3c, 0x68, 0x32, 0x53, 0x0e, 0x4f, 0xf4, 0x34, 0xcb, 0x3f, 0xc1, + 0x0d, 0xcb, 0x81, 0x8b, 0x61, 0x07, 0x37, 0x57, 0x6d, 0x1b, 0x6b, 0x54, 0xf0, 0x45, 0x59, 0x9a, + 0x38, 0xae, 0xe9, 0x3e, 0xe8, 0xd8, 0xc4, 0x3a, 0x67, 0xb3, 0x0a, 0x1e, 0x03, 0x1c, 0x12, 0xff, + 0x5b, 0xd5, 0x5e, 0x17, 0xc6, 0x31, 0x14, 0x81, 0xb6, 0xbb, 0x2c, 0x19, 0x2d, 0xb8, 0xb5, 0x72, + 0x7f, 0x5e, 0xf2, 0x08, 0x6a, 0xda, 0x43, 0x68, 0xc3, 0x2e, 0xbb, 0x2b, 0xb8, 0x6e, 0x01, 0x14, + 0x6f, 0x39, 0xac, 0xec, 0x3b, 0x1f, 0xeb, 0xfa, 0x37, 0x7d, 0xff, 0x57, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x69, 0x33, 0x08, 0xdb, 0xde, 0x07, 0x00, 0x00, } diff --git a/registry/proto/registry.micro.go b/registry/service/proto/registry.pb.micro.go similarity index 99% rename from registry/proto/registry.micro.go rename to registry/service/proto/registry.pb.micro.go index aa6c6851..254935ed 100644 --- a/registry/proto/registry.micro.go +++ b/registry/service/proto/registry.pb.micro.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: registry.proto +// source: micro/go-micro/registry/service/proto/registry.proto package go_micro_registry diff --git a/registry/proto/registry.proto b/registry/service/proto/registry.proto similarity index 86% rename from registry/proto/registry.proto rename to registry/service/proto/registry.proto index 6883102a..ddc09669 100644 --- a/registry/proto/registry.proto +++ b/registry/service/proto/registry.proto @@ -12,11 +12,12 @@ service Registry { // Service represents a go-micro service message Service { - string name = 1; - string version = 2; - map metadata = 3; - repeated Endpoint endpoints = 4; - repeated Node nodes = 5; + string name = 1; + string version = 2; + map metadata = 3; + repeated Endpoint endpoints = 4; + repeated Node nodes = 5; + Options options = 6; } // Node represents the node the service is on @@ -42,6 +43,11 @@ message Value { repeated Value values = 3; } +// Options are registry options +message Options { + int64 ttl = 1; +} + // Result is returns by the watcher message Result { string action = 1; // create, update, delete diff --git a/registry/service/service.go b/registry/service/service.go index 4af5ab40..86f1becc 100644 --- a/registry/service/service.go +++ b/registry/service/service.go @@ -7,7 +7,7 @@ import ( "github.com/micro/go-micro/client" "github.com/micro/go-micro/registry" - pb "github.com/micro/go-micro/registry/proto" + pb "github.com/micro/go-micro/registry/service/proto" ) var ( @@ -58,8 +58,12 @@ func (s *serviceRegistry) Register(srv *registry.Service, opts ...registry.Regis o(&options) } + // encode srv into protobuf and pack Register TTL into it + pbSrv := ToProto(srv) + pbSrv.Options.Ttl = int64(options.TTL.Seconds()) + // register the service - _, err := s.client.Register(context.TODO(), ToProto(srv), s.callOpts()...) + _, err := s.client.Register(context.TODO(), pbSrv, s.callOpts()...) if err != nil { return err } @@ -85,7 +89,7 @@ func (s *serviceRegistry) GetService(name string) ([]*registry.Service, error) { return nil, err } - var services []*registry.Service + services := make([]*registry.Service, 0, len(rsp.Services)) for _, service := range rsp.Services { services = append(services, ToService(service)) } @@ -98,7 +102,7 @@ func (s *serviceRegistry) ListServices() ([]*registry.Service, error) { return nil, err } - var services []*registry.Service + services := make([]*registry.Service, 0, len(rsp.Services)) for _, service := range rsp.Services { services = append(services, ToService(service)) } diff --git a/registry/service/util.go b/registry/service/util.go index bbde4095..ad74ce0c 100644 --- a/registry/service/util.go +++ b/registry/service/util.go @@ -2,7 +2,7 @@ package service import ( "github.com/micro/go-micro/registry" - pb "github.com/micro/go-micro/registry/proto" + pb "github.com/micro/go-micro/registry/service/proto" ) func values(v []*registry.Value) []*pb.Value { @@ -10,7 +10,7 @@ func values(v []*registry.Value) []*pb.Value { return []*pb.Value{} } - var vs []*pb.Value + vs := make([]*pb.Value, 0, len(v)) for _, vi := range v { vs = append(vs, &pb.Value{ Name: vi.Name, @@ -26,7 +26,7 @@ func toValues(v []*pb.Value) []*registry.Value { return []*registry.Value{} } - var vs []*registry.Value + vs := make([]*registry.Value, 0, len(v)) for _, vi := range v { vs = append(vs, ®istry.Value{ Name: vi.Name, @@ -38,7 +38,7 @@ func toValues(v []*pb.Value) []*registry.Value { } func ToProto(s *registry.Service) *pb.Service { - var endpoints []*pb.Endpoint + endpoints := make([]*pb.Endpoint, 0, len(s.Endpoints)) for _, ep := range s.Endpoints { var request, response *pb.Value @@ -66,7 +66,7 @@ func ToProto(s *registry.Service) *pb.Service { }) } - var nodes []*pb.Node + nodes := make([]*pb.Node, 0, len(s.Nodes)) for _, node := range s.Nodes { nodes = append(nodes, &pb.Node{ @@ -86,7 +86,7 @@ func ToProto(s *registry.Service) *pb.Service { } func ToService(s *pb.Service) *registry.Service { - var endpoints []*registry.Endpoint + endpoints := make([]*registry.Endpoint, 0, len(s.Endpoints)) for _, ep := range s.Endpoints { var request, response *registry.Value @@ -114,7 +114,7 @@ func ToService(s *pb.Service) *registry.Service { }) } - var nodes []*registry.Node + nodes := make([]*registry.Node, 0, len(s.Nodes)) for _, node := range s.Nodes { nodes = append(nodes, ®istry.Node{ Id: node.Id, diff --git a/registry/service/watcher.go b/registry/service/watcher.go index 08f2144d..973f9547 100644 --- a/registry/service/watcher.go +++ b/registry/service/watcher.go @@ -2,7 +2,7 @@ package service import ( "github.com/micro/go-micro/registry" - pb "github.com/micro/go-micro/registry/proto" + pb "github.com/micro/go-micro/registry/service/proto" ) type serviceWatcher struct { @@ -11,24 +11,22 @@ type serviceWatcher struct { } func (s *serviceWatcher) Next() (*registry.Result, error) { - for { - // check if closed - select { - case <-s.closed: - return nil, registry.ErrWatcherStopped - default: - } - - r, err := s.stream.Recv() - if err != nil { - return nil, err - } - - return ®istry.Result{ - Action: r.Action, - Service: ToService(r.Service), - }, nil + // check if closed + select { + case <-s.closed: + return nil, registry.ErrWatcherStopped + default: } + + r, err := s.stream.Recv() + if err != nil { + return nil, err + } + + return ®istry.Result{ + Action: r.Action, + Service: ToService(r.Service), + }, nil } func (s *serviceWatcher) Stop() { diff --git a/registry/watcher_test.go b/registry/watcher_test.go index 94c8c275..717dbde3 100644 --- a/registry/watcher_test.go +++ b/registry/watcher_test.go @@ -1,16 +1,18 @@ package registry import ( + "os" "testing" + "time" ) func TestWatcher(t *testing.T) { testData := []*Service{ - &Service{ + { Name: "test1", Version: "1.0.1", Nodes: []*Node{ - &Node{ + { Id: "test1-1", Address: "10.0.0.1:10001", Metadata: map[string]string{ @@ -19,11 +21,11 @@ func TestWatcher(t *testing.T) { }, }, }, - &Service{ + { Name: "test2", Version: "1.0.2", Nodes: []*Node{ - &Node{ + { Id: "test2-1", Address: "10.0.0.2:10002", Metadata: map[string]string{ @@ -32,11 +34,11 @@ func TestWatcher(t *testing.T) { }, }, }, - &Service{ + { Name: "test3", Version: "1.0.3", Nodes: []*Node{ - &Node{ + { Id: "test3-1", Address: "10.0.0.3:10003", Metadata: map[string]string{ @@ -76,8 +78,16 @@ func TestWatcher(t *testing.T) { } } + travis := os.Getenv("TRAVIS") + + var opts []Option + + if travis == "true" { + opts = append(opts, Timeout(time.Millisecond*100)) + } + // new registry - r := NewRegistry() + r := NewRegistry(opts...) w, err := r.Watch() if err != nil { diff --git a/router/default.go b/router/default.go index aa5561ee..459488df 100644 --- a/router/default.go +++ b/router/default.go @@ -13,30 +13,23 @@ import ( "github.com/micro/go-micro/util/log" ) -const ( - // AdvertiseEventsTick is time interval in which the router advertises route updates - AdvertiseEventsTick = 5 * time.Second - // AdvertiseTableTick is time interval in which router advertises all routes found in routing table - AdvertiseTableTick = 1 * time.Minute - // AdvertiseFlushTick is time the yet unconsumed advertisements are flush i.e. discarded - AdvertiseFlushTick = 15 * time.Second - // AdvertSuppress is advert suppression threshold - AdvertSuppress = 2000.0 - // AdvertRecover is advert recovery threshold - AdvertRecover = 750.0 - // DefaultAdvertTTL is default advertisement TTL - DefaultAdvertTTL = 1 * time.Minute - // DeletePenalty penalises route deletion - DeletePenalty = 1000.0 - // UpdatePenalty penalises route updates - UpdatePenalty = 500.0 - // PenaltyHalfLife is the time the advert penalty decays to half its value - PenaltyHalfLife = 2.0 - // MaxSuppressTime defines time after which the suppressed advert is deleted - MaxSuppressTime = 5 * time.Minute -) - var ( + // AdvertiseEventsTick is time interval in which the router advertises route updates + AdvertiseEventsTick = 10 * time.Second + // AdvertiseTableTick is time interval in which router advertises all routes found in routing table + AdvertiseTableTick = 2 * time.Minute + // DefaultAdvertTTL is default advertisement TTL + DefaultAdvertTTL = 2 * time.Minute + // AdvertSuppress is advert suppression threshold + AdvertSuppress = 200.0 + // AdvertRecover is advert recovery threshold + AdvertRecover = 20.0 + // Penalty for routes processed multiple times + Penalty = 100.0 + // PenaltyHalfLife is the time the advert penalty decays to half its value + PenaltyHalfLife = 30.0 + // MaxSuppressTime defines time after which the suppressed advert is deleted + MaxSuppressTime = 90 * time.Second // PenaltyDecay is a coefficient which controls the speed the advert penalty decays PenaltyDecay = math.Log(2) / PenaltyHalfLife ) @@ -54,6 +47,7 @@ type router struct { wg *sync.WaitGroup // advert subscribers + sub sync.RWMutex subscribers map[string]chan *Advert } @@ -191,7 +185,6 @@ func (r *router) watchRegistry(w registry.Watcher) error { defer func() { // close the exit channel when the go routine finishes close(exit) - r.wg.Done() }() // wait in the background for the router to stop @@ -199,6 +192,7 @@ func (r *router) watchRegistry(w registry.Watcher) error { r.wg.Add(1) go func() { defer w.Stop() + defer r.wg.Done() select { case <-r.exit: @@ -235,7 +229,6 @@ func (r *router) watchTable(w Watcher) error { defer func() { // close the exit channel when the go routine finishes close(exit) - r.wg.Done() }() // wait in the background for the router to stop @@ -243,6 +236,7 @@ func (r *router) watchTable(w Watcher) error { r.wg.Add(1) go func() { defer w.Stop() + defer r.wg.Done() select { case <-r.exit: @@ -278,10 +272,7 @@ func (r *router) watchTable(w Watcher) error { } // publishAdvert publishes router advert to advert channel -// NOTE: this might cease to be a dedicated method in the future func (r *router) publishAdvert(advType AdvertType, events []*Event) { - defer r.advertWg.Done() - a := &Advert{ Id: r.options.Id, Type: advType, @@ -290,24 +281,17 @@ func (r *router) publishAdvert(advType AdvertType, events []*Event) { Events: events, } - log.Debugf("Router publishing advert; %+v", a) - r.RLock() + r.sub.RLock() for _, sub := range r.subscribers { - // check the exit chan first - select { - case <-r.exit: - r.RUnlock() - return - default: - } - // now send the message select { case sub <- a: - default: + case <-r.exit: + r.sub.RUnlock() + return } } - r.RUnlock() + r.sub.RUnlock() } // advertiseTable advertises the whole routing table to the network @@ -319,27 +303,20 @@ func (r *router) advertiseTable() error { for { select { case <-ticker.C: - // list routing table routes to announce - routes, err := r.table.List() + // do full table flush + events, err := r.flushRouteEvents(Update) if err != nil { - return fmt.Errorf("failed listing routes: %s", err) - } - // collect all the added routes before we attempt to add default gateway - events := make([]*Event, len(routes)) - for i, route := range routes { - event := &Event{ - Type: Update, - Timestamp: time.Now(), - Route: route, - } - events[i] = event + return fmt.Errorf("failed flushing routes: %s", err) } - // advertise all routes as Update events to subscribers + // advertise routes to subscribers if len(events) > 0 { log.Debugf("Router flushing table with %d events: %s", len(events), r.options.Id) r.advertWg.Add(1) - go r.publishAdvert(RouteUpdate, events) + go func() { + defer r.advertWg.Done() + r.publishAdvert(RouteUpdate, events) + }() } case <-r.exit: return nil @@ -347,12 +324,12 @@ func (r *router) advertiseTable() error { } } -// routeAdvert contains a route event to be advertised -type routeAdvert struct { +// advert contains a route event to be advertised +type advert struct { // event received from routing table event *Event - // lastUpdate records the time of the last advert update - lastUpdate time.Time + // lastSeen records the time of the last advert update + lastSeen time.Time // penalty is current advert penalty penalty float64 // isSuppressed flags the advert suppression @@ -361,6 +338,51 @@ type routeAdvert struct { suppressTime time.Time } +// adverts maintains a map of router adverts +type adverts map[uint64]*advert + +// process processes advert +// It updates advert timestamp, increments its penalty and +// marks upresses or recovers it if it reaches configured thresholds +func (m adverts) process(a *advert) error { + // lookup advert in adverts + hash := a.event.Route.Hash() + a, ok := m[hash] + if !ok { + return fmt.Errorf("advert not found") + } + + // decay the event penalty + delta := time.Since(a.lastSeen).Seconds() + + // decay advert penalty + a.penalty = a.penalty * math.Exp(-delta*PenaltyDecay) + service := a.event.Route.Service + address := a.event.Route.Address + + // suppress/recover the event based on its penalty level + switch { + case a.penalty > AdvertSuppress && !a.isSuppressed: + log.Debugf("Router suppressing advert %d %.2f for route %s %s", hash, a.penalty, service, address) + a.isSuppressed = true + a.suppressTime = time.Now() + case a.penalty < AdvertRecover && a.isSuppressed: + log.Debugf("Router recovering advert %d %.2f for route %s %s", hash, a.penalty, service, address) + a.isSuppressed = false + } + + // if suppressed, checked how long has it been suppressed for + if a.isSuppressed { + // max suppression time threshold has been reached, delete the advert + if time.Since(a.suppressTime) > MaxSuppressTime { + delete(m, hash) + return nil + } + } + + return nil +} + // advertiseEvents advertises routing table events // It suppresses unhealthy flapping events and advertises healthy events upstream. func (r *router) advertiseEvents() error { @@ -368,8 +390,8 @@ func (r *router) advertiseEvents() error { ticker := time.NewTicker(AdvertiseEventsTick) defer ticker.Stop() - // advertMap is a map of advert events - advertMap := make(map[uint64]*routeAdvert) + // adverts is a map of advert events + adverts := make(adverts) // routing table watcher tableWatcher, err := r.Watch() @@ -389,84 +411,98 @@ func (r *router) advertiseEvents() error { for { select { case <-ticker.C: - var events []*Event - // collect all events which are not flapping - for key, advert := range advertMap { - // decay the event penalty - delta := time.Since(advert.lastUpdate).Seconds() - advert.penalty = advert.penalty * math.Exp(-delta*PenaltyDecay) - - // suppress/recover the event based on its penalty level - switch { - case advert.penalty > AdvertSuppress && !advert.isSuppressed: - log.Debugf("Router supressing advert %d for route %s", key, advert.event.Route.Address) - advert.isSuppressed = true - advert.suppressTime = time.Now() - case advert.penalty < AdvertRecover && advert.isSuppressed: - log.Debugf("Router recovering advert %d for route %s", key, advert.event.Route.Address) - advert.isSuppressed = false - } - - // max suppression time threshold has been reached, delete the advert - if advert.isSuppressed { - if time.Since(advert.suppressTime) > MaxSuppressTime { - delete(advertMap, key) - continue - } - } - - if !advert.isSuppressed { - e := new(Event) - *e = *(advert.event) - events = append(events, e) - // delete the advert from the advertMap - delete(advertMap, key) - } + // If we're not advertising any events then sip processing them entirely + if r.options.Advertise == AdvertiseNone { + continue } - // advertise all Update events to subscribers + var events []*Event + + // collect all events which are not flapping + for key, advert := range adverts { + // process the advert + if err := adverts.process(advert); err != nil { + log.Debugf("Router failed processing advert %d: %v", key, err) + continue + } + // if suppressed go to the next advert + if advert.isSuppressed { + continue + } + + // if we only advertise local routes skip processing anything not link local + if r.options.Advertise == AdvertiseLocal && advert.event.Route.Link != "local" { + continue + } + + // copy the event and append + e := new(Event) + // this is ok, because router.Event only contains builtin types + // and no references so this creates a deep copy of struct Event + *e = *(advert.event) + events = append(events, e) + // delete the advert from adverts + delete(adverts, key) + } + + // advertise events to subscribers if len(events) > 0 { - r.advertWg.Add(1) log.Debugf("Router publishing %d events", len(events)) - go r.publishAdvert(RouteUpdate, events) + r.advertWg.Add(1) + go func() { + defer r.advertWg.Done() + r.publishAdvert(RouteUpdate, events) + }() } case e := <-r.eventChan: // if event is nil, continue if e == nil { continue } - log.Debugf("Router processing table event %s for service %s", e.Type, e.Route.Address) - // determine the event penalty - var penalty float64 - switch e.Type { - case Update: - penalty = UpdatePenalty - case Delete: - penalty = DeletePenalty - } - // check if we have already registered the route - hash := e.Route.Hash() - advert, ok := advertMap[hash] - if !ok { - advert = &routeAdvert{ - event: e, - penalty: penalty, - lastUpdate: time.Now(), - } - advertMap[hash] = advert + // If we're not advertising any events then skip processing them entirely + if r.options.Advertise == AdvertiseNone { continue } - // override the route event only if the last event was different - if advert.event.Type != e.Type { - advert.event = e + // if we only advertise local routes skip processing anything not link local + if r.options.Advertise == AdvertiseLocal && e.Route.Link != "local" { + continue + } + + now := time.Now() + + log.Debugf("Router processing table event %s for service %s %s", e.Type, e.Route.Service, e.Route.Address) + + // check if we have already registered the route + hash := e.Route.Hash() + a, ok := adverts[hash] + if !ok { + a = &advert{ + event: e, + penalty: Penalty, + lastSeen: now, + } + adverts[hash] = a + continue + } + + // override the route event only if the previous event was different + if a.event.Type != e.Type { + a.event = e + } + + // process the advert + if err := adverts.process(a); err != nil { + log.Debugf("Router error processing advert %d: %v", hash, err) + continue } // update event penalty and timestamp - advert.lastUpdate = time.Now() - advert.penalty += penalty - log.Debugf("Router advert %d for route %s event penalty: %f", hash, advert.event.Route.Address, advert.penalty) + a.lastSeen = now + // increment the penalty + a.penalty += Penalty + log.Debugf("Router advert %d for route %s %s event penalty: %f", hash, a.event.Route.Service, a.event.Route.Address, a.penalty) case <-r.exit: // first wait for the advertiser to finish r.advertWg.Wait() @@ -477,9 +513,7 @@ func (r *router) advertiseEvents() error { // close closes exit channels func (r *router) close() { - // notify all goroutines to finish - close(r.exit) - + log.Debugf("Router closing remaining channels") // drain the advertise channel only if advertising if r.status.Code == Advertising { // drain the event channel @@ -488,11 +522,18 @@ func (r *router) close() { // close advert subscribers for id, sub := range r.subscribers { + select { + case <-sub: + default: + } + // close the channel close(sub) // delete the subscriber + r.sub.Lock() delete(r.subscribers, id) + r.sub.Unlock() } } @@ -514,6 +555,9 @@ func (r *router) watchErrors() { defer r.Unlock() // if the router is not stopped, stop it if r.status.Code != Stopped { + // notify all goroutines to finish + close(r.exit) + // close all the channels r.close() // set the status error @@ -615,9 +659,16 @@ func (r *router) Advertise() (<-chan *Advert, error) { // create event channels r.eventChan = make(chan *Event) + // create advert channel + advertChan := make(chan *Advert, 128) + r.subscribers[uuid.New().String()] = advertChan + // advertise your presence r.advertWg.Add(1) - go r.publishAdvert(Announce, events) + go func() { + defer r.advertWg.Done() + r.publishAdvert(Announce, events) + }() r.wg.Add(1) go func() { @@ -641,10 +692,7 @@ func (r *router) Advertise() (<-chan *Advert, error) { // mark router as Running and set its Error to nil r.status = Status{Code: Advertising, Error: nil} - // create advert channel - advertChan := make(chan *Advert, 128) - r.subscribers[uuid.New().String()] = advertChan - + log.Debugf("Router starting to advertise") return advertChan, nil case Stopped: return nil, fmt.Errorf("not running") @@ -664,19 +712,21 @@ func (r *router) Process(a *Advert) error { return events[i].Timestamp.Before(events[j].Timestamp) }) - log.Debugf("Router %s processing advert from: %s", r.options.Id, a.Id) + log.Tracef("Router %s processing advert from: %s", r.options.Id, a.Id) for _, event := range events { // skip if the router is the origin of this route if event.Route.Router == r.options.Id { - log.Debugf("Router skipping processing its own route: %s", r.options.Id) + log.Tracef("Router skipping processing its own route: %s", r.options.Id) continue } // create a copy of the route route := event.Route action := event.Type - log.Debugf("Router %s applying %s from router %s for address: %s", r.options.Id, action, route.Router, route.Address) - if err := r.manageRoute(route, fmt.Sprintf("%s", action)); err != nil { + + log.Tracef("Router %s applying %s from router %s for service %s %s", r.options.Id, action, route.Router, route.Service, route.Address) + + if err := r.manageRoute(route, action.String()); err != nil { return fmt.Errorf("failed applying action %s to routing table: %s", action, err) } } @@ -686,21 +736,81 @@ func (r *router) Process(a *Advert) error { // flushRouteEvents returns a slice of events, one per each route in the routing table func (r *router) flushRouteEvents(evType EventType) ([]*Event, error) { + // Do not advertise anything + if r.options.Advertise == AdvertiseNone { + return []*Event{}, nil + } + // list all routes routes, err := r.table.List() if err != nil { return nil, fmt.Errorf("failed listing routes: %s", err) } + // Return all the routes + if r.options.Advertise == AdvertiseAll { + // build a list of events to advertise + events := make([]*Event, len(routes)) + for i, route := range routes { + event := &Event{ + Type: evType, + Timestamp: time.Now(), + Route: route, + } + events[i] = event + } + return events, nil + } + + // routeMap stores the routes we're going to advertise + bestRoutes := make(map[string]Route) + + // set whether we're advertising only local + advertiseLocal := r.options.Advertise == AdvertiseLocal + + // go through all routes found in the routing table and collapse them to optimal routes + for _, route := range routes { + // if we're only advertising local routes + if advertiseLocal && route.Link != "local" { + continue + } + + // now we're going to find the best routes + + routeKey := route.Service + "@" + route.Network + current, ok := bestRoutes[routeKey] + if !ok { + bestRoutes[routeKey] = route + continue + } + // if the current optimal route metric is higher than routing table route, replace it + if current.Metric > route.Metric { + bestRoutes[routeKey] = route + continue + } + // if the metrics are the same, prefer advertising your own route + if current.Metric == route.Metric { + if route.Router == r.options.Id { + bestRoutes[routeKey] = route + continue + } + } + } + + log.Debugf("Router advertising %d %s routes out of %d", len(bestRoutes), r.options.Advertise, len(routes)) + // build a list of events to advertise - events := make([]*Event, len(routes)) - for i, route := range routes { + events := make([]*Event, len(bestRoutes)) + var i int + + for _, route := range bestRoutes { event := &Event{ Type: evType, Timestamp: time.Now(), Route: route, } events[i] = event + i++ } return events, nil @@ -716,14 +826,18 @@ func (r *router) Solicit() error { // advertise the routes r.advertWg.Add(1) - go r.publishAdvert(RouteUpdate, events) + + go func() { + r.publishAdvert(Solicitation, events) + r.advertWg.Done() + }() return nil } // Lookup routes in the routing table -func (r *router) Lookup(q Query) ([]Route, error) { - return r.table.Query(q) +func (r *router) Lookup(q ...QueryOption) ([]Route, error) { + return r.table.Query(q...) } // Watch routes @@ -745,19 +859,30 @@ func (r *router) Status() Status { // Stop stops the router func (r *router) Stop() error { r.Lock() - defer r.Unlock() + + log.Debugf("Router shutting down") switch r.status.Code { case Stopped, Error: + r.Unlock() return r.status.Error case Running, Advertising: + // notify all goroutines to finish + close(r.exit) + // close all the channels + // NOTE: close marks the router status as Stopped r.close() } + r.Unlock() + + log.Tracef("Router waiting for all goroutines to finish") // wait for all goroutines to finish r.wg.Wait() + log.Debugf("Router successfully stopped") + return nil } diff --git a/router/default_test.go b/router/default_test.go new file mode 100644 index 00000000..d3edf67d --- /dev/null +++ b/router/default_test.go @@ -0,0 +1,132 @@ +package router + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/micro/go-micro/registry/memory" + "github.com/micro/go-micro/util/log" +) + +func routerTestSetup() Router { + r := memory.NewRegistry() + return newRouter(Registry(r)) +} + +func TestRouterStartStop(t *testing.T) { + r := routerTestSetup() + + log.Debugf("TestRouterStartStop STARTING") + if err := r.Start(); err != nil { + t.Errorf("failed to start router: %v", err) + } + + _, err := r.Advertise() + if err != nil { + t.Errorf("failed to start advertising: %v", err) + } + + if err := r.Stop(); err != nil { + t.Errorf("failed to stop router: %v", err) + } + log.Debugf("TestRouterStartStop STOPPED") +} + +func TestRouterAdvertise(t *testing.T) { + r := routerTestSetup() + + // lower the advertise interval + AdvertiseEventsTick = 500 * time.Millisecond + AdvertiseTableTick = 1 * time.Second + + if err := r.Start(); err != nil { + t.Errorf("failed to start router: %v", err) + } + + ch, err := r.Advertise() + if err != nil { + t.Errorf("failed to start advertising: %v", err) + } + + // receive announce event + ann := <-ch + log.Debugf("received announce advert: %v", ann) + + // Generate random unique routes + nrRoutes := 5 + routes := make([]Route, nrRoutes) + route := Route{ + Service: "dest.svc", + Address: "dest.addr", + Gateway: "dest.gw", + Network: "dest.network", + Router: "src.router", + Link: "local", + Metric: 10, + } + + for i := 0; i < nrRoutes; i++ { + testRoute := route + testRoute.Service = fmt.Sprintf("%s-%d", route.Service, i) + routes[i] = testRoute + } + + var advertErr error + + createDone := make(chan bool) + errChan := make(chan error) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + wg.Done() + defer close(createDone) + for _, route := range routes { + log.Debugf("Creating route %v", route) + if err := r.Table().Create(route); err != nil { + log.Debugf("Failed to create route: %v", err) + errChan <- err + return + } + } + }() + + var adverts int + readDone := make(chan bool) + + wg.Add(1) + go func() { + defer func() { + wg.Done() + readDone <- true + }() + for advert := range ch { + select { + case advertErr = <-errChan: + t.Errorf("failed advertising events: %v", advertErr) + default: + // do nothing for now + log.Debugf("Router advert received: %v", advert) + adverts += len(advert.Events) + } + return + } + }() + + // done adding routes to routing table + <-createDone + // done reading adverts from the routing table + <-readDone + + if adverts != nrRoutes { + t.Errorf("Expected %d adverts, received: %d", nrRoutes, adverts) + } + + wg.Wait() + + if err := r.Stop(); err != nil { + t.Errorf("failed to stop router: %v", err) + } +} diff --git a/router/handler/router.go b/router/handler/router.go deleted file mode 100644 index 6c681742..00000000 --- a/router/handler/router.go +++ /dev/null @@ -1,196 +0,0 @@ -package handler - -import ( - "context" - "io" - "time" - - "github.com/micro/go-micro/errors" - "github.com/micro/go-micro/router" - pb "github.com/micro/go-micro/router/proto" -) - -// Router implements router handler -type Router struct { - Router router.Router -} - -// Lookup looks up routes in the routing table and returns them -func (r *Router) Lookup(ctx context.Context, req *pb.LookupRequest, resp *pb.LookupResponse) error { - query := router.NewQuery( - router.QueryService(req.Query.Service), - ) - - routes, err := r.Router.Lookup(query) - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to lookup routes: %v", err) - } - - var respRoutes []*pb.Route - for _, route := range routes { - respRoute := &pb.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int64(route.Metric), - } - respRoutes = append(respRoutes, respRoute) - } - - resp.Routes = respRoutes - - return nil -} - -// Solicit triggers full routing table advertisement -func (r *Router) Solicit(ctx context.Context, req *pb.Request, resp *pb.Response) error { - if err := r.Router.Solicit(); err != nil { - return err - } - - return nil -} - -// Advertise streams router advertisements -func (r *Router) Advertise(ctx context.Context, req *pb.Request, stream pb.Router_AdvertiseStream) error { - advertChan, err := r.Router.Advertise() - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to get adverts: %v", err) - } - - for advert := range advertChan { - var events []*pb.Event - for _, event := range advert.Events { - route := &pb.Route{ - Service: event.Route.Service, - Address: event.Route.Address, - Gateway: event.Route.Gateway, - Network: event.Route.Network, - Router: event.Route.Router, - Link: event.Route.Link, - Metric: int64(event.Route.Metric), - } - e := &pb.Event{ - Type: pb.EventType(event.Type), - Timestamp: event.Timestamp.UnixNano(), - Route: route, - } - events = append(events, e) - } - - advert := &pb.Advert{ - Id: advert.Id, - Type: pb.AdvertType(advert.Type), - Timestamp: advert.Timestamp.UnixNano(), - Events: events, - } - - // send the advert - err := stream.Send(advert) - if err == io.EOF { - return nil - } - if err != nil { - return errors.InternalServerError("go.micro.router", "error sending message %v", err) - } - } - - return nil -} - -// Process processes advertisements -func (r *Router) Process(ctx context.Context, req *pb.Advert, rsp *pb.ProcessResponse) error { - events := make([]*router.Event, len(req.Events)) - for i, event := range req.Events { - route := router.Route{ - Service: event.Route.Service, - Address: event.Route.Address, - Gateway: event.Route.Gateway, - Network: event.Route.Network, - Router: event.Route.Router, - Link: event.Route.Link, - Metric: int(event.Route.Metric), - } - - events[i] = &router.Event{ - Type: router.EventType(event.Type), - Timestamp: time.Unix(0, event.Timestamp), - Route: route, - } - } - - advert := &router.Advert{ - Id: req.Id, - Type: router.AdvertType(req.Type), - Timestamp: time.Unix(0, req.Timestamp), - TTL: time.Duration(req.Ttl), - Events: events, - } - - if err := r.Router.Process(advert); err != nil { - return errors.InternalServerError("go.micro.router", "error publishing advert: %v", err) - } - - return nil -} - -// Status returns router status -func (r *Router) Status(ctx context.Context, req *pb.Request, rsp *pb.StatusResponse) error { - status := r.Router.Status() - - rsp.Status = &pb.Status{ - Code: status.Code.String(), - } - - if status.Error != nil { - rsp.Status.Error = status.Error.Error() - } - - return nil -} - -// Watch streans routing table events -func (r *Router) Watch(ctx context.Context, req *pb.WatchRequest, stream pb.Router_WatchStream) error { - watcher, err := r.Router.Watch() - if err != nil { - return errors.InternalServerError("go.micro.router", "failed creating event watcher: %v", err) - } - - defer stream.Close() - - for { - event, err := watcher.Next() - if err == router.ErrWatcherStopped { - return errors.InternalServerError("go.micro.router", "watcher stopped") - } - - if err != nil { - return errors.InternalServerError("go.micro.router", "error watching events: %v", err) - } - - route := &pb.Route{ - Service: event.Route.Service, - Address: event.Route.Address, - Gateway: event.Route.Gateway, - Network: event.Route.Network, - Router: event.Route.Router, - Link: event.Route.Link, - Metric: int64(event.Route.Metric), - } - - tableEvent := &pb.Event{ - Type: pb.EventType(event.Type), - Timestamp: event.Timestamp.UnixNano(), - Route: route, - } - - if err := stream.Send(tableEvent); err != nil { - return err - } - } - - return nil -} diff --git a/router/handler/table.go b/router/handler/table.go deleted file mode 100644 index 63e3c96c..00000000 --- a/router/handler/table.go +++ /dev/null @@ -1,119 +0,0 @@ -package handler - -import ( - "context" - - "github.com/micro/go-micro/errors" - "github.com/micro/go-micro/router" - pb "github.com/micro/go-micro/router/proto" -) - -type Table struct { - Router router.Router -} - -func (t *Table) Create(ctx context.Context, route *pb.Route, resp *pb.CreateResponse) error { - err := t.Router.Table().Create(router.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int(route.Metric), - }) - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to create route: %s", err) - } - - return nil -} - -func (t *Table) Update(ctx context.Context, route *pb.Route, resp *pb.UpdateResponse) error { - err := t.Router.Table().Update(router.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int(route.Metric), - }) - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to update route: %s", err) - } - - return nil -} - -func (t *Table) Delete(ctx context.Context, route *pb.Route, resp *pb.DeleteResponse) error { - err := t.Router.Table().Delete(router.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int(route.Metric), - }) - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to delete route: %s", err) - } - - return nil -} - -// List returns all routes in the routing table -func (t *Table) List(ctx context.Context, req *pb.Request, resp *pb.ListResponse) error { - routes, err := t.Router.Table().List() - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to list routes: %s", err) - } - - var respRoutes []*pb.Route - for _, route := range routes { - respRoute := &pb.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int64(route.Metric), - } - respRoutes = append(respRoutes, respRoute) - } - - resp.Routes = respRoutes - - return nil -} - -func (t *Table) Query(ctx context.Context, req *pb.QueryRequest, resp *pb.QueryResponse) error { - query := router.NewQuery( - router.QueryService(req.Query.Service), - ) - - routes, err := t.Router.Table().Query(query) - if err != nil { - return errors.InternalServerError("go.micro.router", "failed to lookup routes: %s", err) - } - - var respRoutes []*pb.Route - for _, route := range routes { - respRoute := &pb.Route{ - Service: route.Service, - Address: route.Address, - Gateway: route.Gateway, - Network: route.Network, - Router: route.Router, - Link: route.Link, - Metric: int64(route.Metric), - } - respRoutes = append(respRoutes, respRoute) - } - - resp.Routes = respRoutes - - return nil -} diff --git a/router/options.go b/router/options.go index 13119e1b..0fa67b4f 100644 --- a/router/options.go +++ b/router/options.go @@ -18,6 +18,8 @@ type Options struct { Network string // Registry is the local registry Registry registry.Registry + // Advertise is the advertising strategy + Advertise Strategy // Client for calling router Client client.Client } @@ -64,12 +66,20 @@ func Registry(r registry.Registry) Option { } } +// Strategy sets route advertising strategy +func Advertise(a Strategy) Option { + return func(o *Options) { + o.Advertise = a + } +} + // DefaultOptions returns router default options func DefaultOptions() Options { return Options{ - Id: uuid.New().String(), - Address: DefaultAddress, - Network: DefaultNetwork, - Registry: registry.DefaultRegistry, + Id: uuid.New().String(), + Address: DefaultAddress, + Network: DefaultNetwork, + Registry: registry.DefaultRegistry, + Advertise: AdvertiseLocal, } } diff --git a/router/query.go b/router/query.go index 3e2c3d38..557d44e0 100644 --- a/router/query.go +++ b/router/query.go @@ -7,6 +7,8 @@ type QueryOption func(*QueryOptions) type QueryOptions struct { // Service is destination service name Service string + // Address of the service + Address string // Gateway is route gateway Gateway string // Network is network address @@ -22,6 +24,13 @@ func QueryService(s string) QueryOption { } } +// QueryAddress sets service to query +func QueryAddress(a string) QueryOption { + return func(o *QueryOptions) { + o.Address = a + } +} + // QueryGateway sets gateway address to query func QueryGateway(g string) QueryOption { return func(o *QueryOptions) { @@ -43,22 +52,12 @@ func QueryRouter(r string) QueryOption { } } -// Query is routing table query -type Query interface { - // Options returns query options - Options() QueryOptions -} - -// query is a basic implementation of Query -type query struct { - opts QueryOptions -} - // NewQuery creates new query and returns it -func NewQuery(opts ...QueryOption) Query { +func NewQuery(opts ...QueryOption) QueryOptions { // default options qopts := QueryOptions{ Service: "*", + Address: "*", Gateway: "*", Network: "*", Router: "*", @@ -68,12 +67,5 @@ func NewQuery(opts ...QueryOption) Query { o(&qopts) } - return &query{ - opts: qopts, - } -} - -// Options returns query options -func (q *query) Options() QueryOptions { - return q.opts + return qopts } diff --git a/router/route.go b/router/route.go index f2768403..ed384e7c 100644 --- a/router/route.go +++ b/router/route.go @@ -8,9 +8,7 @@ var ( // DefaultLink is default network link DefaultLink = "local" // DefaultLocalMetric is default route cost for a local route - DefaultLocalMetric = 1 - // DefaultNetworkMetric is default route cost for a network route - DefaultNetworkMetric = 10 + DefaultLocalMetric int64 = 1 ) // Route is network route @@ -28,7 +26,7 @@ type Route struct { // Link is network link Link string // Metric is the route cost metric - Metric int + Metric int64 } // Hash returns route hash sum. diff --git a/router/router.go b/router/router.go index 3e8f00cd..7758817c 100644 --- a/router/router.go +++ b/router/router.go @@ -31,7 +31,7 @@ type Router interface { // Solicit advertises the whole routing table to the network Solicit() error // Lookup queries routes in the routing table - Lookup(Query) ([]Route, error) + Lookup(...QueryOption) ([]Route, error) // Watch returns a watcher which tracks updates to the routing table Watch(opts ...WatchOption) (Watcher, error) // Start starts the router @@ -55,7 +55,7 @@ type Table interface { // List all routes in the table List() ([]Route, error) // Query routes in the routing table - Query(Query) ([]Route, error) + Query(...QueryOption) ([]Route, error) } // Option used by the router @@ -111,6 +111,8 @@ const ( Announce AdvertType = iota // RouteUpdate advertises route updates RouteUpdate + // Solicitation indicates routes were solicited + Solicitation ) // String returns human readable advertisement type @@ -120,6 +122,8 @@ func (t AdvertType) String() string { return "announce" case RouteUpdate: return "update" + case Solicitation: + return "solicitation" default: return "unknown" } @@ -139,6 +143,36 @@ type Advert struct { Events []*Event } +// Strategy is route advertisement strategy +type Strategy int + +const ( + // AdvertiseAll advertises all routes to the network + AdvertiseAll Strategy = iota + // AdvertiseBest advertises optimal routes to the network + AdvertiseBest + // AdvertiseLocal will only advertise the local routes + AdvertiseLocal + // AdvertiseNone will not advertise any routes + AdvertiseNone +) + +// String returns human readable Strategy +func (s Strategy) String() string { + switch s { + case AdvertiseAll: + return "all" + case AdvertiseBest: + return "best" + case AdvertiseLocal: + return "local" + case AdvertiseNone: + return "none" + default: + return "unknown" + } +} + // NewRouter creates new Router and returns it func NewRouter(opts ...Option) Router { return newRouter(opts...) diff --git a/router/proto/router.pb.go b/router/service/proto/router.pb.go similarity index 82% rename from router/proto/router.pb.go rename to router/service/proto/router.pb.go index 19813a34..4e2c4cc0 100644 --- a/router/proto/router.pb.go +++ b/router/service/proto/router.pb.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: router.proto +// source: github.com/micro/go-micro/router/proto/router.proto package go_micro_router @@ -43,7 +43,7 @@ func (x AdvertType) String() string { } func (AdvertType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{0} + return fileDescriptor_2dd64c6ec344e37e, []int{0} } // EventType defines the type of event @@ -72,7 +72,7 @@ func (x EventType) String() string { } func (EventType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{1} + return fileDescriptor_2dd64c6ec344e37e, []int{1} } // Empty request @@ -86,7 +86,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{0} + return fileDescriptor_2dd64c6ec344e37e, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { @@ -118,7 +118,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{1} + return fileDescriptor_2dd64c6ec344e37e, []int{1} } func (m *Response) XXX_Unmarshal(b []byte) error { @@ -151,7 +151,7 @@ func (m *ListResponse) Reset() { *m = ListResponse{} } func (m *ListResponse) String() string { return proto.CompactTextString(m) } func (*ListResponse) ProtoMessage() {} func (*ListResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{2} + return fileDescriptor_2dd64c6ec344e37e, []int{2} } func (m *ListResponse) XXX_Unmarshal(b []byte) error { @@ -191,7 +191,7 @@ func (m *LookupRequest) Reset() { *m = LookupRequest{} } func (m *LookupRequest) String() string { return proto.CompactTextString(m) } func (*LookupRequest) ProtoMessage() {} func (*LookupRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{3} + return fileDescriptor_2dd64c6ec344e37e, []int{3} } func (m *LookupRequest) XXX_Unmarshal(b []byte) error { @@ -231,7 +231,7 @@ func (m *LookupResponse) Reset() { *m = LookupResponse{} } func (m *LookupResponse) String() string { return proto.CompactTextString(m) } func (*LookupResponse) ProtoMessage() {} func (*LookupResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{4} + return fileDescriptor_2dd64c6ec344e37e, []int{4} } func (m *LookupResponse) XXX_Unmarshal(b []byte) error { @@ -271,7 +271,7 @@ func (m *QueryRequest) Reset() { *m = QueryRequest{} } func (m *QueryRequest) String() string { return proto.CompactTextString(m) } func (*QueryRequest) ProtoMessage() {} func (*QueryRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{5} + return fileDescriptor_2dd64c6ec344e37e, []int{5} } func (m *QueryRequest) XXX_Unmarshal(b []byte) error { @@ -311,7 +311,7 @@ func (m *QueryResponse) Reset() { *m = QueryResponse{} } func (m *QueryResponse) String() string { return proto.CompactTextString(m) } func (*QueryResponse) ProtoMessage() {} func (*QueryResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{6} + return fileDescriptor_2dd64c6ec344e37e, []int{6} } func (m *QueryResponse) XXX_Unmarshal(b []byte) error { @@ -350,7 +350,7 @@ func (m *WatchRequest) Reset() { *m = WatchRequest{} } func (m *WatchRequest) String() string { return proto.CompactTextString(m) } func (*WatchRequest) ProtoMessage() {} func (*WatchRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{7} + return fileDescriptor_2dd64c6ec344e37e, []int{7} } func (m *WatchRequest) XXX_Unmarshal(b []byte) error { @@ -392,7 +392,7 @@ func (m *Advert) Reset() { *m = Advert{} } func (m *Advert) String() string { return proto.CompactTextString(m) } func (*Advert) ProtoMessage() {} func (*Advert) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{8} + return fileDescriptor_2dd64c6ec344e37e, []int{8} } func (m *Advert) XXX_Unmarshal(b []byte) error { @@ -461,7 +461,7 @@ func (m *Solicit) Reset() { *m = Solicit{} } func (m *Solicit) String() string { return proto.CompactTextString(m) } func (*Solicit) ProtoMessage() {} func (*Solicit) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{9} + return fileDescriptor_2dd64c6ec344e37e, []int{9} } func (m *Solicit) XXX_Unmarshal(b []byte) error { @@ -500,7 +500,7 @@ func (m *ProcessResponse) Reset() { *m = ProcessResponse{} } func (m *ProcessResponse) String() string { return proto.CompactTextString(m) } func (*ProcessResponse) ProtoMessage() {} func (*ProcessResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{10} + return fileDescriptor_2dd64c6ec344e37e, []int{10} } func (m *ProcessResponse) XXX_Unmarshal(b []byte) error { @@ -532,7 +532,7 @@ func (m *CreateResponse) Reset() { *m = CreateResponse{} } func (m *CreateResponse) String() string { return proto.CompactTextString(m) } func (*CreateResponse) ProtoMessage() {} func (*CreateResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{11} + return fileDescriptor_2dd64c6ec344e37e, []int{11} } func (m *CreateResponse) XXX_Unmarshal(b []byte) error { @@ -564,7 +564,7 @@ func (m *DeleteResponse) Reset() { *m = DeleteResponse{} } func (m *DeleteResponse) String() string { return proto.CompactTextString(m) } func (*DeleteResponse) ProtoMessage() {} func (*DeleteResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{12} + return fileDescriptor_2dd64c6ec344e37e, []int{12} } func (m *DeleteResponse) XXX_Unmarshal(b []byte) error { @@ -596,7 +596,7 @@ func (m *UpdateResponse) Reset() { *m = UpdateResponse{} } func (m *UpdateResponse) String() string { return proto.CompactTextString(m) } func (*UpdateResponse) ProtoMessage() {} func (*UpdateResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{13} + return fileDescriptor_2dd64c6ec344e37e, []int{13} } func (m *UpdateResponse) XXX_Unmarshal(b []byte) error { @@ -634,7 +634,7 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{14} + return fileDescriptor_2dd64c6ec344e37e, []int{14} } func (m *Event) XXX_Unmarshal(b []byte) error { @@ -693,7 +693,7 @@ func (m *Query) Reset() { *m = Query{} } func (m *Query) String() string { return proto.CompactTextString(m) } func (*Query) ProtoMessage() {} func (*Query) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{15} + return fileDescriptor_2dd64c6ec344e37e, []int{15} } func (m *Query) XXX_Unmarshal(b []byte) error { @@ -760,7 +760,7 @@ func (m *Route) Reset() { *m = Route{} } func (m *Route) String() string { return proto.CompactTextString(m) } func (*Route) ProtoMessage() {} func (*Route) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{16} + return fileDescriptor_2dd64c6ec344e37e, []int{16} } func (m *Route) XXX_Unmarshal(b []byte) error { @@ -842,7 +842,7 @@ func (m *Status) Reset() { *m = Status{} } func (m *Status) String() string { return proto.CompactTextString(m) } func (*Status) ProtoMessage() {} func (*Status) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{17} + return fileDescriptor_2dd64c6ec344e37e, []int{17} } func (m *Status) XXX_Unmarshal(b []byte) error { @@ -888,7 +888,7 @@ func (m *StatusResponse) Reset() { *m = StatusResponse{} } func (m *StatusResponse) String() string { return proto.CompactTextString(m) } func (*StatusResponse) ProtoMessage() {} func (*StatusResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_367072455c71aedc, []int{18} + return fileDescriptor_2dd64c6ec344e37e, []int{18} } func (m *StatusResponse) XXX_Unmarshal(b []byte) error { @@ -940,53 +940,56 @@ func init() { proto.RegisterType((*StatusResponse)(nil), "go.micro.router.StatusResponse") } -func init() { proto.RegisterFile("router.proto", fileDescriptor_367072455c71aedc) } - -var fileDescriptor_367072455c71aedc = []byte{ - // 714 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xcd, 0x4e, 0xdb, 0x40, - 0x10, 0xb6, 0x93, 0xd8, 0x69, 0xa6, 0x21, 0xa4, 0xa3, 0x0a, 0x4c, 0x5a, 0x20, 0xf2, 0x09, 0x21, - 0x64, 0xaa, 0xf4, 0xda, 0x1f, 0x02, 0xa5, 0xaa, 0x54, 0x0e, 0xad, 0x01, 0xf5, 0x6c, 0xec, 0x15, - 0xb5, 0x48, 0xbc, 0x66, 0x77, 0x03, 0xca, 0xb9, 0x4f, 0xd3, 0x4b, 0x2f, 0x7d, 0xa4, 0xbe, 0x48, - 0xe5, 0xdd, 0x75, 0x08, 0x71, 0x16, 0x09, 0x4e, 0xd9, 0xf9, 0xfb, 0x66, 0x66, 0xf7, 0x9b, 0x71, - 0xa0, 0xcd, 0xe8, 0x44, 0x10, 0x16, 0xe4, 0x8c, 0x0a, 0x8a, 0xab, 0x97, 0x34, 0x18, 0xa7, 0x31, - 0xa3, 0x81, 0x52, 0xfb, 0x2d, 0x68, 0x86, 0xe4, 0x7a, 0x42, 0xb8, 0xf0, 0x01, 0x9e, 0x85, 0x84, - 0xe7, 0x34, 0xe3, 0xc4, 0xff, 0x00, 0xed, 0x93, 0x94, 0x8b, 0x52, 0xc6, 0x00, 0x5c, 0x19, 0xc0, - 0x3d, 0xbb, 0x5f, 0xdf, 0x79, 0x3e, 0x58, 0x0b, 0x16, 0x80, 0x82, 0xb0, 0xf8, 0x09, 0xb5, 0x97, - 0xff, 0x1e, 0x56, 0x4e, 0x28, 0xbd, 0x9a, 0xe4, 0x1a, 0x1c, 0xf7, 0xc0, 0xb9, 0x9e, 0x10, 0x36, - 0xf5, 0xec, 0xbe, 0xbd, 0x34, 0xfe, 0x7b, 0x61, 0x0d, 0x95, 0x93, 0x7f, 0x00, 0x9d, 0x32, 0xfc, - 0x89, 0x05, 0xbc, 0x83, 0xb6, 0x42, 0x7c, 0x52, 0xfe, 0x8f, 0xb0, 0xa2, 0xa3, 0x9f, 0x98, 0xbe, - 0x03, 0xed, 0x1f, 0x91, 0x88, 0x7f, 0x96, 0x77, 0xfb, 0xdb, 0x06, 0x77, 0x98, 0xdc, 0x10, 0x26, - 0xb0, 0x03, 0xb5, 0x34, 0x91, 0x65, 0xb4, 0xc2, 0x5a, 0x9a, 0xe0, 0x3e, 0x34, 0xc4, 0x34, 0x27, - 0x5e, 0xad, 0x6f, 0xef, 0x74, 0x06, 0xaf, 0x2a, 0xc0, 0x2a, 0xec, 0x6c, 0x9a, 0x93, 0x50, 0x3a, - 0xe2, 0x6b, 0x68, 0x89, 0x74, 0x4c, 0xb8, 0x88, 0xc6, 0xb9, 0x57, 0xef, 0xdb, 0x3b, 0xf5, 0xf0, - 0x4e, 0x81, 0x5d, 0xa8, 0x0b, 0x31, 0xf2, 0x1a, 0x52, 0x5f, 0x1c, 0x8b, 0xda, 0xc9, 0x0d, 0xc9, - 0x04, 0xf7, 0x1c, 0x43, 0xed, 0xc7, 0x85, 0x39, 0xd4, 0x5e, 0xfe, 0x06, 0x34, 0x4f, 0xe9, 0x28, - 0x8d, 0xd3, 0x4a, 0xad, 0xfe, 0x0b, 0x58, 0xfd, 0xc6, 0x68, 0x4c, 0x38, 0x9f, 0x31, 0xa5, 0x0b, - 0x9d, 0x23, 0x46, 0x22, 0x41, 0xe6, 0x35, 0x9f, 0xc8, 0x88, 0xdc, 0xd7, 0x9c, 0xe7, 0xc9, 0xbc, - 0xcf, 0x2f, 0x1b, 0x1c, 0x99, 0x15, 0x03, 0xdd, 0xbe, 0x2d, 0xdb, 0xef, 0x2d, 0xaf, 0xcd, 0xd4, - 0x7d, 0x6d, 0xb1, 0xfb, 0x3d, 0x70, 0x64, 0x9c, 0xbc, 0x17, 0xf3, 0x33, 0x29, 0x27, 0xff, 0x1c, - 0x1c, 0xf9, 0xcc, 0xe8, 0x41, 0x93, 0x13, 0x76, 0x93, 0xc6, 0x44, 0x37, 0x5b, 0x8a, 0x85, 0xe5, - 0x32, 0x12, 0xe4, 0x36, 0x9a, 0xca, 0x64, 0xad, 0xb0, 0x14, 0x0b, 0x4b, 0x46, 0xc4, 0x2d, 0x65, - 0x57, 0x32, 0x59, 0x2b, 0x2c, 0x45, 0xff, 0xaf, 0x0d, 0x8e, 0xcc, 0xf3, 0x30, 0x6e, 0x94, 0x24, - 0x8c, 0x70, 0x5e, 0xe2, 0x6a, 0x71, 0x3e, 0x63, 0xdd, 0x98, 0xb1, 0x71, 0x2f, 0x23, 0xae, 0x69, - 0x7a, 0x32, 0xcf, 0x91, 0x06, 0x2d, 0x21, 0x42, 0x63, 0x94, 0x66, 0x57, 0x9e, 0x2b, 0xb5, 0xf2, - 0x5c, 0xf8, 0x8e, 0x89, 0x60, 0x69, 0xec, 0x35, 0xe5, 0xed, 0x69, 0xc9, 0x1f, 0x80, 0x7b, 0x2a, - 0x22, 0x31, 0xe1, 0x45, 0x54, 0x4c, 0x93, 0xb2, 0x64, 0x79, 0xc6, 0x97, 0xe0, 0x10, 0xc6, 0x28, - 0xd3, 0xd5, 0x2a, 0xc1, 0x1f, 0x42, 0x47, 0xc5, 0xcc, 0x06, 0x65, 0x1f, 0x5c, 0x2e, 0x35, 0x7a, - 0xd0, 0xd6, 0x2b, 0x2f, 0xa0, 0x03, 0xb4, 0xdb, 0xee, 0x00, 0xe0, 0x8e, 0xe1, 0x88, 0xd0, 0x51, - 0xd2, 0x30, 0xcb, 0xe8, 0x24, 0x8b, 0x49, 0xd7, 0xc2, 0x2e, 0xb4, 0x95, 0x4e, 0x71, 0xa8, 0x6b, - 0xef, 0xee, 0x43, 0x6b, 0x46, 0x0b, 0x04, 0x70, 0x15, 0x01, 0xbb, 0x56, 0x71, 0x56, 0xd4, 0xeb, - 0xda, 0xc5, 0x59, 0x07, 0xd4, 0x06, 0x7f, 0xea, 0xe0, 0x86, 0xea, 0x4a, 0xbe, 0x82, 0xab, 0x56, - 0x0b, 0x6e, 0x55, 0x4a, 0xbb, 0xb7, 0xb2, 0x7a, 0xdb, 0x46, 0xbb, 0x26, 0xb1, 0x85, 0x87, 0xe0, - 0xc8, 0x31, 0xc7, 0xcd, 0x8a, 0xef, 0xfc, 0xf8, 0xf7, 0x0c, 0x23, 0xe7, 0x5b, 0x6f, 0x6c, 0x3c, - 0x84, 0x96, 0x6a, 0x2f, 0xe5, 0x04, 0xbd, 0x2a, 0x61, 0x35, 0xc4, 0xba, 0x61, 0x31, 0x48, 0x8c, - 0x83, 0xbb, 0x91, 0x35, 0x23, 0x6c, 0x2c, 0xb1, 0xcc, 0x3a, 0xf9, 0x0c, 0x4d, 0x3d, 0xd9, 0x68, - 0xca, 0xd4, 0xeb, 0x57, 0x0c, 0x8b, 0xcb, 0xc0, 0xc2, 0xe3, 0x19, 0x8b, 0xcc, 0x85, 0x6c, 0x9b, - 0x38, 0x31, 0x83, 0x19, 0xfc, 0xab, 0x81, 0x73, 0x16, 0x5d, 0x8c, 0x08, 0x1e, 0x95, 0xcf, 0x8b, - 0x86, 0x61, 0x5e, 0x02, 0xb7, 0xb0, 0x90, 0xac, 0x02, 0x44, 0xf1, 0xe2, 0x11, 0x20, 0x0b, 0x3b, - 0x4c, 0x82, 0x28, 0x42, 0x3d, 0x02, 0x64, 0x61, 0xed, 0x59, 0x38, 0x84, 0x46, 0xf1, 0x61, 0x7d, - 0xe0, 0x76, 0xaa, 0x54, 0x9a, 0xff, 0x12, 0xfb, 0x16, 0x7e, 0x29, 0xb7, 0xd6, 0xa6, 0xe1, 0x23, - 0xa6, 0x81, 0xb6, 0x4c, 0xe6, 0x12, 0xe9, 0xc2, 0x95, 0x7f, 0x0a, 0xde, 0xfe, 0x0f, 0x00, 0x00, - 0xff, 0xff, 0xb7, 0x25, 0xac, 0xac, 0x24, 0x08, 0x00, 0x00, +func init() { + proto.RegisterFile("github.com/micro/go-micro/router/proto/router.proto", fileDescriptor_2dd64c6ec344e37e) +} + +var fileDescriptor_2dd64c6ec344e37e = []byte{ + // 736 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0x4f, 0x4f, 0xdb, 0x4a, + 0x10, 0xb7, 0x93, 0xd8, 0x79, 0x99, 0x17, 0x42, 0xde, 0xe8, 0x09, 0x4c, 0xde, 0x03, 0x22, 0x9f, + 0x10, 0xa2, 0x4e, 0x15, 0xae, 0xfd, 0x43, 0xa0, 0x54, 0x95, 0xca, 0xa1, 0x35, 0xa0, 0x9e, 0x8d, + 0xb3, 0x0a, 0x16, 0x49, 0xd6, 0xec, 0xae, 0x41, 0x39, 0xf7, 0xd3, 0xf4, 0xd2, 0x4b, 0x3f, 0x52, + 0xbf, 0x48, 0xe5, 0xdd, 0x75, 0x08, 0x71, 0x16, 0x09, 0x4e, 0xd9, 0xf9, 0xf7, 0x9b, 0x99, 0xdd, + 0xdf, 0x8c, 0x03, 0x87, 0xa3, 0x44, 0x5c, 0x67, 0x57, 0x41, 0x4c, 0x27, 0xbd, 0x49, 0x12, 0x33, + 0xda, 0x1b, 0xd1, 0x57, 0xea, 0xc0, 0x68, 0x26, 0x08, 0xeb, 0xa5, 0x8c, 0x8a, 0x42, 0x08, 0xa4, + 0x80, 0xeb, 0x23, 0x1a, 0x48, 0x9f, 0x40, 0xa9, 0xfd, 0x06, 0xd4, 0x43, 0x72, 0x9b, 0x11, 0x2e, + 0x7c, 0x80, 0xbf, 0x42, 0xc2, 0x53, 0x3a, 0xe5, 0xc4, 0x7f, 0x07, 0xcd, 0xb3, 0x84, 0x8b, 0x42, + 0xc6, 0x00, 0x5c, 0x19, 0xc0, 0x3d, 0xbb, 0x5b, 0xdd, 0xfb, 0xbb, 0xbf, 0x11, 0x2c, 0x01, 0x05, + 0x61, 0xfe, 0x13, 0x6a, 0x2f, 0xff, 0x2d, 0xac, 0x9d, 0x51, 0x7a, 0x93, 0xa5, 0x1a, 0x1c, 0x0f, + 0xc0, 0xb9, 0xcd, 0x08, 0x9b, 0x79, 0x76, 0xd7, 0x5e, 0x19, 0xff, 0x35, 0xb7, 0x86, 0xca, 0xc9, + 0x3f, 0x82, 0x56, 0x11, 0xfe, 0xc2, 0x02, 0xde, 0x40, 0x53, 0x21, 0xbe, 0x28, 0xff, 0x7b, 0x58, + 0xd3, 0xd1, 0x2f, 0x4c, 0xdf, 0x82, 0xe6, 0xb7, 0x48, 0xc4, 0xd7, 0xc5, 0xdd, 0xfe, 0xb0, 0xc1, + 0x1d, 0x0c, 0xef, 0x08, 0x13, 0xd8, 0x82, 0x4a, 0x32, 0x94, 0x65, 0x34, 0xc2, 0x4a, 0x32, 0xc4, + 0x1e, 0xd4, 0xc4, 0x2c, 0x25, 0x5e, 0xa5, 0x6b, 0xef, 0xb5, 0xfa, 0xff, 0x95, 0x80, 0x55, 0xd8, + 0xc5, 0x2c, 0x25, 0xa1, 0x74, 0xc4, 0xff, 0xa1, 0x21, 0x92, 0x09, 0xe1, 0x22, 0x9a, 0xa4, 0x5e, + 0xb5, 0x6b, 0xef, 0x55, 0xc3, 0x07, 0x05, 0xb6, 0xa1, 0x2a, 0xc4, 0xd8, 0xab, 0x49, 0x7d, 0x7e, + 0xcc, 0x6b, 0x27, 0x77, 0x64, 0x2a, 0xb8, 0xe7, 0x18, 0x6a, 0x3f, 0xcd, 0xcd, 0xa1, 0xf6, 0xf2, + 0xb7, 0xa0, 0x7e, 0x4e, 0xc7, 0x49, 0x9c, 0x94, 0x6a, 0xf5, 0xff, 0x81, 0xf5, 0x2f, 0x8c, 0xc6, + 0x84, 0xf3, 0x39, 0x53, 0xda, 0xd0, 0x3a, 0x61, 0x24, 0x12, 0x64, 0x51, 0xf3, 0x81, 0x8c, 0xc9, + 0x63, 0xcd, 0x65, 0x3a, 0x5c, 0xf4, 0xf9, 0x6e, 0x83, 0x23, 0xb3, 0x62, 0xa0, 0xdb, 0xb7, 0x65, + 0xfb, 0x9d, 0xd5, 0xb5, 0x99, 0xba, 0xaf, 0x2c, 0x77, 0x7f, 0x00, 0x8e, 0x8c, 0x93, 0xf7, 0x62, + 0x7e, 0x26, 0xe5, 0xe4, 0x5f, 0x82, 0x23, 0x9f, 0x19, 0x3d, 0xa8, 0x73, 0xc2, 0xee, 0x92, 0x98, + 0xe8, 0x66, 0x0b, 0x31, 0xb7, 0x8c, 0x22, 0x41, 0xee, 0xa3, 0x99, 0x4c, 0xd6, 0x08, 0x0b, 0x31, + 0xb7, 0x4c, 0x89, 0xb8, 0xa7, 0xec, 0x46, 0x26, 0x6b, 0x84, 0x85, 0xe8, 0xff, 0xb2, 0xc1, 0x91, + 0x79, 0x9e, 0xc6, 0x8d, 0x86, 0x43, 0x46, 0x38, 0x2f, 0x70, 0xb5, 0xb8, 0x98, 0xb1, 0x6a, 0xcc, + 0x58, 0x7b, 0x94, 0x11, 0x37, 0x34, 0x3d, 0x99, 0xe7, 0x48, 0x83, 0x96, 0x10, 0xa1, 0x36, 0x4e, + 0xa6, 0x37, 0x9e, 0x2b, 0xb5, 0xf2, 0x9c, 0xfb, 0x4e, 0x88, 0x60, 0x49, 0xec, 0xd5, 0xe5, 0xed, + 0x69, 0xc9, 0xef, 0x83, 0x7b, 0x2e, 0x22, 0x91, 0xf1, 0x3c, 0x2a, 0xa6, 0xc3, 0xa2, 0x64, 0x79, + 0xc6, 0x7f, 0xc1, 0x21, 0x8c, 0x51, 0xa6, 0xab, 0x55, 0x82, 0x3f, 0x80, 0x96, 0x8a, 0x99, 0x0f, + 0x4a, 0x0f, 0x5c, 0x2e, 0x35, 0x7a, 0xd0, 0x36, 0x4b, 0x2f, 0xa0, 0x03, 0xb4, 0xdb, 0x7e, 0x1f, + 0xe0, 0x81, 0xe1, 0x88, 0xd0, 0x52, 0xd2, 0x60, 0x3a, 0xa5, 0xd9, 0x34, 0x26, 0x6d, 0x0b, 0xdb, + 0xd0, 0x54, 0x3a, 0xc5, 0xa1, 0xb6, 0xbd, 0xdf, 0x83, 0xc6, 0x9c, 0x16, 0x08, 0xe0, 0x2a, 0x02, + 0xb6, 0xad, 0xfc, 0xac, 0xa8, 0xd7, 0xb6, 0xf3, 0xb3, 0x0e, 0xa8, 0xf4, 0x7f, 0x56, 0xc1, 0x0d, + 0xd5, 0x95, 0x7c, 0x06, 0x57, 0xad, 0x16, 0xdc, 0x29, 0x95, 0xf6, 0x68, 0x65, 0x75, 0x76, 0x8d, + 0x76, 0x4d, 0x62, 0x0b, 0x8f, 0xc1, 0x91, 0x63, 0x8e, 0xdb, 0x25, 0xdf, 0xc5, 0xf1, 0xef, 0x18, + 0x46, 0xce, 0xb7, 0x5e, 0xdb, 0x78, 0x0c, 0x0d, 0xd5, 0x5e, 0xc2, 0x09, 0x7a, 0x65, 0xc2, 0x6a, + 0x88, 0x4d, 0xc3, 0x62, 0x90, 0x18, 0x47, 0x0f, 0x23, 0x6b, 0x46, 0xd8, 0x5a, 0x61, 0x99, 0x77, + 0xf2, 0x11, 0xea, 0x7a, 0xb2, 0xd1, 0x94, 0xa9, 0xd3, 0x2d, 0x19, 0x96, 0x97, 0x81, 0x85, 0xa7, + 0x73, 0x16, 0x99, 0x0b, 0xd9, 0x35, 0x71, 0x62, 0x0e, 0xd3, 0xff, 0x5d, 0x01, 0xe7, 0x22, 0xba, + 0x1a, 0x13, 0x3c, 0x29, 0x9e, 0x17, 0x0d, 0xc3, 0xbc, 0x02, 0x6e, 0x69, 0x21, 0x59, 0x39, 0x88, + 0xe2, 0xc5, 0x33, 0x40, 0x96, 0x76, 0x98, 0x04, 0x51, 0x84, 0x7a, 0x06, 0xc8, 0xd2, 0xda, 0xb3, + 0x70, 0x00, 0xb5, 0xfc, 0xc3, 0xfa, 0xc4, 0xed, 0x94, 0xa9, 0xb4, 0xf8, 0x25, 0xf6, 0x2d, 0xfc, + 0x54, 0x6c, 0xad, 0x6d, 0xc3, 0x47, 0x4c, 0x03, 0xed, 0x98, 0xcc, 0x05, 0xd2, 0x95, 0x2b, 0xff, + 0x14, 0x1c, 0xfe, 0x09, 0x00, 0x00, 0xff, 0xff, 0x47, 0x98, 0xd8, 0x20, 0x4b, 0x08, 0x00, 0x00, } diff --git a/router/proto/router.micro.go b/router/service/proto/router.pb.micro.go similarity index 99% rename from router/proto/router.micro.go rename to router/service/proto/router.pb.micro.go index a27af9e6..024989b9 100644 --- a/router/proto/router.micro.go +++ b/router/service/proto/router.pb.micro.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: router.proto +// source: github.com/micro/go-micro/router/proto/router.proto package go_micro_router diff --git a/router/proto/router.proto b/router/service/proto/router.proto similarity index 100% rename from router/proto/router.proto rename to router/service/proto/router.proto diff --git a/router/service/service.go b/router/service/service.go index 7f521e8d..1087da3d 100644 --- a/router/service/service.go +++ b/router/service/service.go @@ -10,7 +10,7 @@ import ( "github.com/micro/go-micro/client" "github.com/micro/go-micro/router" - pb "github.com/micro/go-micro/router/proto" + pb "github.com/micro/go-micro/router/service/proto" ) type svc struct { @@ -132,7 +132,7 @@ func (s *svc) advertiseEvents(advertChan chan *router.Advert, stream pb.Router_A Gateway: event.Route.Gateway, Network: event.Route.Network, Link: event.Route.Link, - Metric: int(event.Route.Metric), + Metric: event.Route.Metric, } events[i] = &router.Event{ @@ -188,7 +188,7 @@ func (s *svc) Advertise() (<-chan *router.Advert, error) { // Process processes incoming adverts func (s *svc) Process(advert *router.Advert) error { - var events []*pb.Event + events := make([]*pb.Event, 0, len(advert.Events)) for _, event := range advert.Events { route := &pb.Route{ Service: event.Route.Service, @@ -196,7 +196,7 @@ func (s *svc) Process(advert *router.Advert) error { Gateway: event.Route.Gateway, Network: event.Route.Network, Link: event.Route.Link, - Metric: int64(event.Route.Metric), + Metric: event.Route.Metric, } e := &pb.Event{ Type: pb.EventType(event.Type), @@ -230,7 +230,7 @@ func (s *svc) Solicit() error { // build events to advertise events := make([]*router.Event, len(routes)) - for i, _ := range events { + for i := range events { events[i] = &router.Event{ Type: router.Update, Timestamp: time.Now(), @@ -321,13 +321,15 @@ func (s *svc) Stop() error { } // Lookup looks up routes in the routing table and returns them -func (s *svc) Lookup(q router.Query) ([]router.Route, error) { +func (s *svc) Lookup(q ...router.QueryOption) ([]router.Route, error) { // call the router + query := router.NewQuery(q...) + resp, err := s.router.Lookup(context.Background(), &pb.LookupRequest{ Query: &pb.Query{ - Service: q.Options().Service, - Gateway: q.Options().Gateway, - Network: q.Options().Network, + Service: query.Service, + Gateway: query.Gateway, + Network: query.Network, }, }, s.callOpts...) @@ -344,7 +346,7 @@ func (s *svc) Lookup(q router.Query) ([]router.Route, error) { Gateway: route.Gateway, Network: route.Network, Link: route.Link, - Metric: int(route.Metric), + Metric: route.Metric, } } diff --git a/router/service/table.go b/router/service/table.go index a6c44fd3..bcfc8ef3 100644 --- a/router/service/table.go +++ b/router/service/table.go @@ -5,7 +5,7 @@ import ( "github.com/micro/go-micro/client" "github.com/micro/go-micro/router" - pb "github.com/micro/go-micro/router/proto" + pb "github.com/micro/go-micro/router/service/proto" ) type table struct { @@ -21,7 +21,7 @@ func (t *table) Create(r router.Route) error { Gateway: r.Gateway, Network: r.Network, Link: r.Link, - Metric: int64(r.Metric), + Metric: r.Metric, } if _, err := t.table.Create(context.Background(), route, t.callOpts...); err != nil { @@ -39,7 +39,7 @@ func (t *table) Delete(r router.Route) error { Gateway: r.Gateway, Network: r.Network, Link: r.Link, - Metric: int64(r.Metric), + Metric: r.Metric, } if _, err := t.table.Delete(context.Background(), route, t.callOpts...); err != nil { @@ -57,7 +57,7 @@ func (t *table) Update(r router.Route) error { Gateway: r.Gateway, Network: r.Network, Link: r.Link, - Metric: int64(r.Metric), + Metric: r.Metric, } if _, err := t.table.Update(context.Background(), route, t.callOpts...); err != nil { @@ -82,7 +82,7 @@ func (t *table) List() ([]router.Route, error) { Gateway: route.Gateway, Network: route.Network, Link: route.Link, - Metric: int(route.Metric), + Metric: route.Metric, } } @@ -90,13 +90,15 @@ func (t *table) List() ([]router.Route, error) { } // Lookup looks up routes in the routing table and returns them -func (t *table) Query(q router.Query) ([]router.Route, error) { +func (t *table) Query(q ...router.QueryOption) ([]router.Route, error) { + query := router.NewQuery(q...) + // call the router resp, err := t.table.Query(context.Background(), &pb.QueryRequest{ Query: &pb.Query{ - Service: q.Options().Service, - Gateway: q.Options().Gateway, - Network: q.Options().Network, + Service: query.Service, + Gateway: query.Gateway, + Network: query.Network, }, }, t.callOpts...) @@ -113,7 +115,7 @@ func (t *table) Query(q router.Query) ([]router.Route, error) { Gateway: route.Gateway, Network: route.Network, Link: route.Link, - Metric: int(route.Metric), + Metric: route.Metric, } } diff --git a/router/service/watcher.go b/router/service/watcher.go index 01124473..663d0fce 100644 --- a/router/service/watcher.go +++ b/router/service/watcher.go @@ -6,7 +6,7 @@ import ( "time" "github.com/micro/go-micro/router" - pb "github.com/micro/go-micro/router/proto" + pb "github.com/micro/go-micro/router/service/proto" ) type watcher struct { @@ -61,7 +61,7 @@ func (w *watcher) watch(stream pb.Router_WatchService) error { Gateway: resp.Route.Gateway, Network: resp.Route.Network, Link: resp.Route.Link, - Metric: int(resp.Route.Metric), + Metric: resp.Route.Metric, } event := &router.Event{ diff --git a/router/table.go b/router/table.go index ecef0c62..2b05ada0 100644 --- a/router/table.go +++ b/router/table.go @@ -135,22 +135,44 @@ func (t *table) List() ([]Route, error) { } // isMatch checks if the route matches given query options -func isMatch(route Route, gateway, network, router string) bool { - if gateway == "*" || gateway == route.Gateway { - if network == "*" || network == route.Network { - if router == "*" || router == route.Router { - return true - } +func isMatch(route Route, address, gateway, network, router string) bool { + // matches the values provided + match := func(a, b string) bool { + if a == "*" || a == b { + return true + } + return false + } + + // a simple struct to hold our values + type compare struct { + a string + b string + } + + // compare the following values + values := []compare{ + {gateway, route.Gateway}, + {network, route.Network}, + {router, route.Router}, + {address, route.Address}, + } + + for _, v := range values { + // attempt to match each value + if !match(v.a, v.b) { + return false } } - return false + + return true } // findRoutes finds all the routes for given network and router and returns them -func findRoutes(routes map[uint64]Route, gateway, network, router string) []Route { +func findRoutes(routes map[uint64]Route, address, gateway, network, router string) []Route { var results []Route for _, route := range routes { - if isMatch(route, gateway, network, router) { + if isMatch(route, address, gateway, network, router) { results = append(results, route) } } @@ -158,21 +180,24 @@ func findRoutes(routes map[uint64]Route, gateway, network, router string) []Rout } // Lookup queries routing table and returns all routes that match the lookup query -func (t *table) Query(q Query) ([]Route, error) { +func (t *table) Query(q ...QueryOption) ([]Route, error) { t.RLock() defer t.RUnlock() - if q.Options().Service != "*" { - if _, ok := t.routes[q.Options().Service]; !ok { + // create new query options + opts := NewQuery(q...) + + if opts.Service != "*" { + if _, ok := t.routes[opts.Service]; !ok { return nil, ErrRouteNotFound } - return findRoutes(t.routes[q.Options().Service], q.Options().Gateway, q.Options().Network, q.Options().Router), nil + return findRoutes(t.routes[opts.Service], opts.Address, opts.Gateway, opts.Network, opts.Router), nil } - var results []Route + results := make([]Route, 0, len(t.routes)) // search through all destinations for _, routes := range t.routes { - results = append(results, findRoutes(routes, q.Options().Gateway, q.Options().Network, q.Options().Router)...) + results = append(results, findRoutes(routes, opts.Address, opts.Gateway, opts.Network, opts.Router)...) } return results, nil diff --git a/router/table_test.go b/router/table_test.go index d989ee3b..a05cb956 100644 --- a/router/table_test.go +++ b/router/table_test.go @@ -7,6 +7,7 @@ func testSetup() (*table, Route) { route := Route{ Service: "dest.svc", + Address: "dest.addr", Gateway: "dest.gw", Network: "dest.network", Router: "src.router", @@ -123,18 +124,17 @@ func TestQuery(t *testing.T) { } // return all routes - query := NewQuery() - - routes, err := table.Query(query) + routes, err := table.Query() if err != nil { t.Errorf("error looking up routes: %s", err) + } else if len(routes) == 0 { + t.Errorf("error looking up routes: not found") } // query routes particular network network := "net1" - query = NewQuery(QueryNetwork(network)) - routes, err = table.Query(query) + routes, err = table.Query(QueryNetwork(network)) if err != nil { t.Errorf("error looking up routes: %s", err) } @@ -151,9 +151,8 @@ func TestQuery(t *testing.T) { // query routes for particular gateway gateway := "gw1" - query = NewQuery(QueryGateway(gateway)) - routes, err = table.Query(query) + routes, err = table.Query(QueryGateway(gateway)) if err != nil { t.Errorf("error looking up routes: %s", err) } @@ -168,9 +167,8 @@ func TestQuery(t *testing.T) { // query routes for particular router router := "rtr1" - query = NewQuery(QueryRouter(router)) - routes, err = table.Query(query) + routes, err = table.Query(QueryRouter(router)) if err != nil { t.Errorf("error looking up routes: %s", err) } @@ -184,13 +182,13 @@ func TestQuery(t *testing.T) { } // query particular gateway and network - query = NewQuery( + query := []QueryOption{ QueryGateway(gateway), QueryNetwork(network), QueryRouter(router), - ) + } - routes, err = table.Query(query) + routes, err = table.Query(query...) if err != nil { t.Errorf("error looking up routes: %s", err) } @@ -212,9 +210,7 @@ func TestQuery(t *testing.T) { } // non-existen route query - query = NewQuery(QueryService("foobar")) - - routes, err = table.Query(query) + routes, err = table.Query(QueryService("foobar")) if err != ErrRouteNotFound { t.Errorf("error looking up routes. Expected: %s, found: %s", ErrRouteNotFound, err) } diff --git a/runtime/package/package.go b/runtime/build/build.go similarity index 52% rename from runtime/package/package.go rename to runtime/build/build.go index 078c8a59..41a4beeb 100644 --- a/runtime/package/package.go +++ b/runtime/build/build.go @@ -1,16 +1,16 @@ -// Package packager creates a binary image. Due to package being a reserved keyword we use packager. -package packager +// Package build builds a micro runtime package +package build import ( "github.com/micro/go-micro/runtime/source" ) -// Package builds binaries -type Packager interface { - // Compile builds a binary - Compile(*Source) (*Binary, error) - // Deletes the binary - Delete(*Binary) error +// Builder builds binaries +type Builder interface { + // Build builds a package + Build(*Source) (*Package, error) + // Clean deletes the package + Clean(*Package) error } // Source is the source of a build @@ -21,8 +21,8 @@ type Source struct { Repository *source.Repository } -// Binary is the representation of a binary -type Binary struct { +// Package is micro service package +type Package struct { // Name of the binary Name string // Location of the binary diff --git a/runtime/package/docker/docker.go b/runtime/build/docker/docker.go similarity index 78% rename from runtime/package/docker/docker.go rename to runtime/build/docker/docker.go index 2f0c46c0..d71c53ef 100644 --- a/runtime/package/docker/docker.go +++ b/runtime/build/docker/docker.go @@ -8,17 +8,17 @@ import ( "os" "path/filepath" - "github.com/fsouza/go-dockerclient" - "github.com/micro/go-micro/runtime/package" + docker "github.com/fsouza/go-dockerclient" + "github.com/micro/go-micro/runtime/build" "github.com/micro/go-micro/util/log" ) -type Packager struct { - Options packager.Options +type Builder struct { + Options build.Options Client *docker.Client } -func (d *Packager) Compile(s *packager.Source) (*packager.Binary, error) { +func (d *Builder) Build(s *build.Source) (*build.Package, error) { image := filepath.Join(s.Repository.Path, s.Repository.Name) buf := new(bytes.Buffer) @@ -63,7 +63,7 @@ func (d *Packager) Compile(s *packager.Source) (*packager.Binary, error) { if err != nil { return nil, err } - return &packager.Binary{ + return &build.Package{ Name: image, Path: image, Type: "docker", @@ -71,13 +71,13 @@ func (d *Packager) Compile(s *packager.Source) (*packager.Binary, error) { }, nil } -func (d *Packager) Delete(b *packager.Binary) error { +func (d *Builder) Clean(b *build.Package) error { image := filepath.Join(b.Path, b.Name) return d.Client.RemoveImage(image) } -func NewPackager(opts ...packager.Option) packager.Packager { - options := packager.Options{} +func NewBuilder(opts ...build.Option) build.Builder { + options := build.Options{} for _, o := range opts { o(&options) } @@ -86,7 +86,7 @@ func NewPackager(opts ...packager.Option) packager.Packager { if err != nil { log.Fatal(err) } - return &Packager{ + return &Builder{ Options: options, Client: client, } diff --git a/runtime/package/go/golang.go b/runtime/build/go/golang.go similarity index 74% rename from runtime/package/go/golang.go rename to runtime/build/go/golang.go index a9a591ab..435da0b5 100644 --- a/runtime/package/go/golang.go +++ b/runtime/build/go/golang.go @@ -6,11 +6,11 @@ import ( "os/exec" "path/filepath" - "github.com/micro/go-micro/runtime/package" + "github.com/micro/go-micro/runtime/build" ) -type Packager struct { - Options packager.Options +type Builder struct { + Options build.Options Cmd string Path string } @@ -34,7 +34,7 @@ func whichGo() string { return "go" } -func (g *Packager) Compile(s *packager.Source) (*packager.Binary, error) { +func (g *Builder) Build(s *build.Source) (*build.Package, error) { binary := filepath.Join(g.Path, s.Repository.Name) source := filepath.Join(s.Repository.Path, s.Repository.Name) @@ -42,7 +42,7 @@ func (g *Packager) Compile(s *packager.Source) (*packager.Binary, error) { if err := cmd.Run(); err != nil { return nil, err } - return &packager.Binary{ + return &build.Package{ Name: s.Repository.Name, Path: binary, Type: "go", @@ -50,19 +50,19 @@ func (g *Packager) Compile(s *packager.Source) (*packager.Binary, error) { }, nil } -func (g *Packager) Delete(b *packager.Binary) error { +func (g *Builder) Clean(b *build.Package) error { binary := filepath.Join(b.Path, b.Name) return os.Remove(binary) } -func NewPackager(opts ...packager.Option) packager.Packager { - options := packager.Options{ +func NewBuild(opts ...build.Option) build.Builder { + options := build.Options{ Path: os.TempDir(), } for _, o := range opts { o(&options) } - return &Packager{ + return &Builder{ Options: options, Cmd: whichGo(), Path: options.Path, diff --git a/runtime/package/options.go b/runtime/build/options.go similarity index 92% rename from runtime/package/options.go rename to runtime/build/options.go index a308c737..65e7dd68 100644 --- a/runtime/package/options.go +++ b/runtime/build/options.go @@ -1,4 +1,4 @@ -package packager +package build type Options struct { // local path to download source diff --git a/runtime/default.go b/runtime/default.go index 9ebf7a06..11ab7d32 100644 --- a/runtime/default.go +++ b/runtime/default.go @@ -2,19 +2,16 @@ package runtime import ( "errors" - "io" - "strings" "sync" "time" - "github.com/micro/go-micro/runtime/package" - "github.com/micro/go-micro/runtime/process" - proc "github.com/micro/go-micro/runtime/process/os" "github.com/micro/go-micro/util/log" ) type runtime struct { sync.RWMutex + // options configure runtime + options Options // used to stop the runtime closed chan bool // used to start new services @@ -22,164 +19,73 @@ type runtime struct { // indicates if we're running running bool // the service map + // TODO: track different versions of the same service services map[string]*service } -type service struct { - sync.RWMutex +// NewRuntime creates new local runtime and returns it +func NewRuntime(opts ...Option) Runtime { + // get default options + options := Options{} - running bool - closed chan bool - err error + // apply requested options + for _, o := range opts { + o(&options) + } - // output for logs - output io.Writer - - // service to manage - *Service - // process creator - Process *proc.Process - // Exec - Exec *process.Executable - // process pid - PID *process.PID -} - -func newRuntime() *runtime { return &runtime{ + options: options, closed: make(chan bool), start: make(chan *service, 128), services: make(map[string]*service), } } -func newService(s *Service, c CreateOptions) *service { - var exec string - var args []string +// Init initializes runtime options +func (r *runtime) Init(opts ...Option) error { + r.Lock() + defer r.Unlock() - if len(s.Exec) > 0 { - parts := strings.Split(s.Exec, " ") - exec = parts[0] - args = []string{} - - if len(parts) > 1 { - args = parts[1:] - } - } else { - // set command - exec = c.Command[0] - // set args - if len(c.Command) > 1 { - args = c.Command[1:] - } - } - - return &service{ - Service: s, - Process: new(proc.Process), - Exec: &process.Executable{ - Binary: &packager.Binary{ - Name: s.Name, - Path: exec, - }, - Env: c.Env, - Args: args, - }, - output: c.Output, - } -} - -func (s *service) streamOutput() { - go io.Copy(s.output, s.PID.Output) - go io.Copy(s.output, s.PID.Error) -} - -func (s *service) Running() bool { - s.RLock() - defer s.RUnlock() - return s.running -} - -func (s *service) Start() error { - s.Lock() - defer s.Unlock() - - if s.running { - return nil - } - - // reset - s.err = nil - s.closed = make(chan bool) - - // TODO: pull source & build binary - log.Debugf("Runtime service %s forking new process\n") - p, err := s.Process.Fork(s.Exec) - if err != nil { - return err - } - - // set the pid - s.PID = p - // set to running - s.running = true - - if s.output != nil { - s.streamOutput() - } - - // wait and watch - go s.Wait() - - return nil -} - -func (s *service) Stop() error { - s.Lock() - defer s.Unlock() - - select { - case <-s.closed: - return nil - default: - close(s.closed) - s.running = false - return s.Process.Kill(s.PID) + for _, o := range opts { + o(&r.options) } return nil } -func (s *service) Error() error { - s.RLock() - defer s.RUnlock() - return s.err -} - -func (s *service) Wait() { - // wait for process to exit - err := s.Process.Wait(s.PID) - - s.Lock() - defer s.Unlock() - - // save the error - if err != nil { - s.err = err - } - - // no longer running - s.running = false -} - -func (r *runtime) run() { - r.RLock() - closed := r.closed - r.RUnlock() - +// run runs the runtime management loop +func (r *runtime) run(events <-chan Event) { t := time.NewTicker(time.Second * 5) defer t.Stop() + // process event processes an incoming event + processEvent := func(event Event, service *service) error { + // get current vals + r.RLock() + name := service.Name + updated := service.updated + r.RUnlock() + + // only process if the timestamp is newer + if !event.Timestamp.After(updated) { + return nil + } + + log.Debugf("Runtime updating service %s", name) + + // this will cause a delete followed by created + if err := r.Update(service.Service); err != nil { + return err + } + + // update the local timestamp + r.Lock() + service.updated = updated + r.Unlock() + + return nil + } + for { select { case <-t.C: @@ -201,19 +107,49 @@ func (r *runtime) run() { if service.Running() { continue } - // TODO: check service error - log.Debugf("Starting %s", service.Name) + log.Debugf("Runtime starting service %s", service.Name) if err := service.Start(); err != nil { - log.Debugf("Runtime error starting %s: %v", service.Name, err) + log.Debugf("Runtime error starting service %s: %v", service.Name, err) } - case <-closed: - // TODO: stop all the things + case event := <-events: + log.Debugf("Runtime received notification event: %v", event) + // NOTE: we only handle Update events for now + switch event.Type { + case Update: + if len(event.Service) > 0 { + r.RLock() + service, ok := r.services[event.Service] + r.RUnlock() + if !ok { + log.Debugf("Runtime unknown service: %s", event.Service) + continue + } + if err := processEvent(event, service); err != nil { + log.Debugf("Runtime error updating service %s: %v", event.Service, err) + } + continue + } + + r.RLock() + services := r.services + r.RUnlock() + + // if blank service was received we update all services + for _, service := range services { + if err := processEvent(event, service); err != nil { + log.Debugf("Runtime error updating service %s: %v", service.Name, err) + } + } + } + case <-r.closed: + log.Debugf("Runtime stopped.") return } } } +// Create creates a new service which is then started by runtime func (r *runtime) Create(s *Service, opts ...CreateOption) error { r.Lock() defer r.Unlock() @@ -227,7 +163,7 @@ func (r *runtime) Create(s *Service, opts ...CreateOption) error { o(&options) } - if len(s.Exec) == 0 && len(options.Command) == 0 { + if len(options.Command) == 0 { return errors.New("missing exec command") } @@ -235,23 +171,107 @@ func (r *runtime) Create(s *Service, opts ...CreateOption) error { r.services[s.Name] = newService(s, options) // push into start queue + log.Debugf("Runtime creating service %s", s.Name) r.start <- r.services[s.Name] return nil } +// Read returns all instances of requested service +// If no service name is provided we return all the track services. +func (r *runtime) Read(opts ...ReadOption) ([]*Service, error) { + r.Lock() + defer r.Unlock() + + gopts := ReadOptions{} + for _, o := range opts { + o(&gopts) + } + + save := func(k, v string) bool { + if len(k) == 0 { + return true + } + return k == v + } + + //nolint:prealloc + var services []*Service + + for _, service := range r.services { + if !save(gopts.Service, service.Name) { + continue + } + if !save(gopts.Version, service.Version) { + continue + } + // TODO deal with service type + // no version has sbeen requested, just append the service + services = append(services, service.Service) + } + + return services, nil +} + +// Update attemps to update the service +func (r *runtime) Update(s *Service) error { + var opts []CreateOption + + // check if the service already exists + r.RLock() + if service, ok := r.services[s.Name]; ok { + opts = append(opts, WithOutput(service.output)) + } + r.RUnlock() + + // delete the service + if err := r.Delete(s); err != nil { + return err + } + + // create new service + return r.Create(s, opts...) +} + +// Delete removes the service from the runtime and stops it func (r *runtime) Delete(s *Service) error { r.Lock() defer r.Unlock() + log.Debugf("Runtime deleting service %s", s.Name) if s, ok := r.services[s.Name]; ok { + // check if running + if !s.Running() { + delete(r.services, s.Name) + return nil + } + // otherwise stop it + if err := s.Stop(); err != nil { + return err + } + // delete it delete(r.services, s.Name) - return s.Stop() + return nil } return nil } +// List returns a slice of all services tracked by the runtime +func (r *runtime) List() ([]*Service, error) { + r.RLock() + defer r.RUnlock() + + services := make([]*Service, 0, len(r.services)) + + for _, service := range r.services { + services = append(services, service.Service) + } + + return services, nil +} + +// Start starts the runtime func (r *runtime) Start() error { r.Lock() defer r.Unlock() @@ -265,11 +285,22 @@ func (r *runtime) Start() error { r.running = true r.closed = make(chan bool) - go r.run() + var events <-chan Event + if r.options.Notifier != nil { + var err error + events, err = r.options.Notifier.Notify() + if err != nil { + // TODO: should we bail here? + log.Debugf("Runtime failed to start update notifier") + } + } + + go r.run(events) return nil } +// Stop stops the runtime func (r *runtime) Stop() error { r.Lock() defer r.Unlock() @@ -292,7 +323,16 @@ func (r *runtime) Stop() error { log.Debugf("Runtime stopping %s", service.Name) service.Stop() } + // stop the notifier too + if r.options.Notifier != nil { + return r.options.Notifier.Close() + } } return nil } + +// String implements stringer interface +func (r *runtime) String() string { + return "local" +} diff --git a/runtime/kubernetes/client/api/api_test.go b/runtime/kubernetes/client/api/api_test.go new file mode 100644 index 00000000..474dff55 --- /dev/null +++ b/runtime/kubernetes/client/api/api_test.go @@ -0,0 +1,169 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +type testcase struct { + Token string + ReqFn func(opts *Options) *Request + Method string + URI string + Body interface{} + Header map[string]string + Assert func(req *http.Request) bool +} + +type assertFn func(req *http.Request) bool + +var tests = []testcase{ + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Get().Resource("service") + }, + Method: "GET", + URI: "/api/v1/namespaces/default/services/", + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Get().Resource("service").Name("foo") + }, + Method: "GET", + URI: "/api/v1/namespaces/default/services/foo", + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Get().Resource("service").Namespace("test").Name("bar") + }, + Method: "GET", + URI: "/api/v1/namespaces/test/services/bar", + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Get().Resource("deployment").Name("foo") + }, + Method: "GET", + URI: "/apis/apps/v1/namespaces/default/deployments/foo", + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Get().Resource("deployment").Namespace("test").Name("foo") + }, + Method: "GET", + URI: "/apis/apps/v1/namespaces/test/deployments/foo", + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Get().Resource("pod").Params(&Params{LabelSelector: map[string]string{"foo": "bar"}}) + }, + Method: "GET", + URI: "/api/v1/namespaces/default/pods/?labelSelector=foo%3Dbar", + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Post().Resource("service").Name("foo").Body(map[string]string{"foo": "bar"}) + }, + Method: "POST", + URI: "/api/v1/namespaces/default/services/foo", + Body: map[string]string{"foo": "bar"}, + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Post().Resource("deployment").Namespace("test").Name("foo").Body(map[string]string{"foo": "bar"}) + }, + Method: "POST", + URI: "/apis/apps/v1/namespaces/test/deployments/foo", + Body: map[string]string{"foo": "bar"}, + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Put().Resource("endpoint").Name("baz").Body(map[string]string{"bam": "bar"}) + }, + Method: "PUT", + URI: "/api/v1/namespaces/default/endpoints/baz", + Body: map[string]string{"bam": "bar"}, + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Patch().Resource("endpoint").Name("baz").Body(map[string]string{"bam": "bar"}) + }, + Method: "PATCH", + URI: "/api/v1/namespaces/default/endpoints/baz", + Body: map[string]string{"bam": "bar"}, + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Patch().Resource("endpoint").Name("baz").SetHeader("foo", "bar") + }, + Method: "PATCH", + URI: "/api/v1/namespaces/default/endpoints/baz", + Header: map[string]string{"foo": "bar"}, + }, + testcase{ + ReqFn: func(opts *Options) *Request { + return NewRequest(opts).Patch().Resource("deployment").Name("baz").SetHeader("foo", "bar") + }, + Method: "PATCH", + URI: "/apis/apps/v1/namespaces/default/deployments/baz", + Header: map[string]string{"foo": "bar"}, + }, +} + +var wrappedHandler = func(test *testcase, t *testing.T) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if len(test.Token) > 0 && (len(auth) == 0 || auth != "Bearer "+test.Token) { + t.Errorf("test case token (%s) did not match expected token (%s)", "Bearer "+test.Token, auth) + } + + if len(test.Method) > 0 && test.Method != r.Method { + t.Errorf("test case Method (%s) did not match expected Method (%s)", test.Method, r.Method) + } + + if len(test.URI) > 0 && test.URI != r.URL.RequestURI() { + t.Errorf("test case URI (%s) did not match expected URI (%s)", test.URI, r.URL.RequestURI()) + } + + if test.Body != nil { + var res map[string]string + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&res); err != nil { + t.Errorf("decoding body failed: %v", err) + } + if !reflect.DeepEqual(res, test.Body) { + t.Error("body did not match") + } + } + + if test.Header != nil { + for k, v := range test.Header { + if r.Header.Get(k) != v { + t.Error("header did not exist") + } + } + } + + w.WriteHeader(http.StatusOK) + }) +} + +func TestRequest(t *testing.T) { + for _, test := range tests { + ts := httptest.NewServer(wrappedHandler(&test, t)) + req := test.ReqFn(&Options{ + Host: ts.URL, + Client: &http.Client{}, + BearerToken: &test.Token, + Namespace: "default", + }) + res := req.Do() + if res.Error() != nil { + t.Errorf("request failed with %v", res.Error()) + } + ts.Close() + } +} diff --git a/runtime/kubernetes/client/api/request.go b/runtime/kubernetes/client/api/request.go new file mode 100644 index 00000000..1b454c00 --- /dev/null +++ b/runtime/kubernetes/client/api/request.go @@ -0,0 +1,228 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/micro/go-micro/util/log" +) + +// Request is used to construct a http request for the k8s API. +type Request struct { + client *http.Client + header http.Header + params url.Values + method string + host string + namespace string + + resource string + resourceName *string + body io.Reader + + err error +} + +// Params is the object to pass in to set paramaters +// on a request. +type Params struct { + LabelSelector map[string]string + Annotations map[string]string +} + +// verb sets method +func (r *Request) verb(method string) *Request { + r.method = method + return r +} + +// Get request +func (r *Request) Get() *Request { + return r.verb("GET") +} + +// Post request +func (r *Request) Post() *Request { + return r.verb("POST") +} + +// Put request +func (r *Request) Put() *Request { + return r.verb("PUT") +} + +// Patch request +func (r *Request) Patch() *Request { + return r.verb("PATCH") +} + +// Delete request +func (r *Request) Delete() *Request { + return r.verb("DELETE") +} + +// Namespace is to set the namespace to operate on +func (r *Request) Namespace(s string) *Request { + r.namespace = s + return r +} + +// Resource is the type of resource the operation is +// for, such as "services", "endpoints" or "pods" +func (r *Request) Resource(s string) *Request { + r.resource = s + return r +} + +// Name is for targeting a specific resource by id +func (r *Request) Name(s string) *Request { + r.resourceName = &s + return r +} + +// Body pass in a body to set, this is for POST, PUT and PATCH requests +func (r *Request) Body(in interface{}) *Request { + b := new(bytes.Buffer) + // if we're not sending YAML request, we encode to JSON + if r.header.Get("Content-Type") != "application/yaml" { + if err := json.NewEncoder(b).Encode(&in); err != nil { + r.err = err + return r + } + log.Debugf("Request body: %v", b) + r.body = b + return r + } + + // if application/yaml is set, we assume we get a raw bytes so we just copy over + body, ok := in.(io.Reader) + if !ok { + r.err = errors.New("invalid data") + return r + } + // copy over data to the bytes buffer + if _, err := io.Copy(b, body); err != nil { + r.err = err + return r + } + + log.Debugf("Request body: %v", b) + r.body = b + return r +} + +// Params isused to set paramters on a request +func (r *Request) Params(p *Params) *Request { + for k, v := range p.LabelSelector { + // create new key=value pair + value := fmt.Sprintf("%s=%s", k, v) + // check if there's an existing value + if label := r.params.Get("labelSelector"); len(label) > 0 { + value = fmt.Sprintf("%s,%s", label, value) + } + // set and overwrite the value + r.params.Set("labelSelector", value) + } + + return r +} + +// SetHeader sets a header on a request with +// a `key` and `value` +func (r *Request) SetHeader(key, value string) *Request { + r.header.Add(key, value) + return r +} + +// request builds the http.Request from the options +func (r *Request) request() (*http.Request, error) { + var url string + switch r.resource { + case "pod", "service", "endpoint": + // /api/v1/namespaces/{namespace}/pods + url = fmt.Sprintf("%s/api/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource) + case "deployment": + // /apis/apps/v1/namespaces/{namespace}/deployments/{name} + url = fmt.Sprintf("%s/apis/apps/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource) + } + + // append resourceName if it is present + if r.resourceName != nil { + url += *r.resourceName + } + + // append any query params + if len(r.params) > 0 { + url += "?" + r.params.Encode() + } + + // build request + req, err := http.NewRequest(r.method, url, r.body) + if err != nil { + return nil, err + } + + // set headers on request + req.Header = r.header + return req, nil +} + +// Do builds and triggers the request +func (r *Request) Do() *Response { + if r.err != nil { + return &Response{ + err: r.err, + } + } + + req, err := r.request() + if err != nil { + return &Response{ + err: err, + } + } + + log.Debugf("kubernetes api request: %v", req) + + res, err := r.client.Do(req) + if err != nil { + return &Response{ + err: err, + } + } + + log.Debugf("kubernetes api response: %v", res) + + // return res, err + return newResponse(res, err) +} + +// Options ... +type Options struct { + Host string + Namespace string + BearerToken *string + Client *http.Client +} + +// NewRequest creates a k8s api request +func NewRequest(opts *Options) *Request { + req := &Request{ + header: make(http.Header), + params: make(url.Values), + client: opts.Client, + namespace: opts.Namespace, + host: opts.Host, + } + + if opts.BearerToken != nil { + req.SetHeader("Authorization", "Bearer "+*opts.BearerToken) + } + + return req +} diff --git a/runtime/kubernetes/client/api/response.go b/runtime/kubernetes/client/api/response.go new file mode 100644 index 00000000..ceed48db --- /dev/null +++ b/runtime/kubernetes/client/api/response.go @@ -0,0 +1,95 @@ +package api + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + + "github.com/micro/go-micro/util/log" +) + +// Errors ... +var ( + ErrNotFound = errors.New("kubernetes: resource not found") + ErrDecode = errors.New("kubernetes: error decoding") + ErrUnknown = errors.New("kubernetes: unknown error") +) + +// Status is an object that is returned when a request +// failed or delete succeeded. +// type Status struct { +// Kind string `json:"kind"` +// Status string `json:"status"` +// Message string `json:"message"` +// Reason string `json:"reason"` +// Code int `json:"code"` +// } + +// Response ... +type Response struct { + res *http.Response + err error + + body []byte +} + +// Error returns an error +func (r *Response) Error() error { + return r.err +} + +// StatusCode returns status code for response +func (r *Response) StatusCode() int { + return r.res.StatusCode +} + +// Into decode body into `data` +func (r *Response) Into(data interface{}) error { + if r.err != nil { + return r.err + } + + defer r.res.Body.Close() + decoder := json.NewDecoder(r.res.Body) + err := decoder.Decode(&data) + if err != nil { + return ErrDecode + } + + return r.err +} + +func newResponse(res *http.Response, err error) *Response { + r := &Response{ + res: res, + err: err, + } + + if err != nil { + return r + } + + if r.res.StatusCode == http.StatusOK || + r.res.StatusCode == http.StatusCreated || + r.res.StatusCode == http.StatusNoContent { + // Non error status code + return r + } + + if r.res.StatusCode == http.StatusNotFound { + r.err = ErrNotFound + return r + } + + log.Logf("kubernetes: request failed with code %v", r.res.StatusCode) + + b, err := ioutil.ReadAll(r.res.Body) + if err == nil { + log.Log("kubernetes: request failed with body:") + log.Log(string(b)) + } + r.err = ErrUnknown + + return r +} diff --git a/runtime/kubernetes/client/client.go b/runtime/kubernetes/client/client.go new file mode 100644 index 00000000..4637a9d8 --- /dev/null +++ b/runtime/kubernetes/client/client.go @@ -0,0 +1,155 @@ +package client + +import ( + "bytes" + "crypto/tls" + "errors" + "io/ioutil" + "net/http" + "os" + "path" + + "github.com/micro/go-micro/runtime/kubernetes/client/api" + "github.com/micro/go-micro/util/log" +) + +var ( + // path to kubernetes service account token + serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount" + // ErrReadNamespace is returned when the names could not be read from service account + ErrReadNamespace = errors.New("Could not read namespace from service account secret") +) + +// Client ... +type client struct { + opts *api.Options +} + +// NewClientInCluster creates a Kubernetes client for use from within a k8s pod. +func NewClientInCluster() *client { + host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT") + + s, err := os.Stat(serviceAccountPath) + if err != nil { + log.Fatal(err) + } + if s == nil || !s.IsDir() { + log.Fatal(errors.New("service account not found")) + } + + token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token")) + if err != nil { + log.Fatal(err) + } + t := string(token) + + ns, err := detectNamespace() + if err != nil { + log.Fatal(err) + } + + crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt")) + if err != nil { + log.Fatal(err) + } + + c := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: crt, + }, + DisableCompression: true, + }, + } + + return &client{ + opts: &api.Options{ + Client: c, + Host: host, + Namespace: ns, + BearerToken: &t, + }, + } +} + +func detectNamespace() (string, error) { + nsPath := path.Join(serviceAccountPath, "namespace") + + // Make sure it's a file and we can read it + if s, e := os.Stat(nsPath); e != nil { + return "", e + } else if s.IsDir() { + return "", ErrReadNamespace + } + + // Read the file, and cast to a string + if ns, e := ioutil.ReadFile(nsPath); e != nil { + return string(ns), e + } else { + return string(ns), nil + } +} + +// Create creates new API object +func (c *client) Create(r *Resource) error { + b := new(bytes.Buffer) + if err := renderTemplate(r.Kind, b, r.Value); err != nil { + return err + } + + return api.NewRequest(c.opts). + Post(). + SetHeader("Content-Type", "application/yaml"). + Resource(r.Kind). + Body(b). + Do(). + Error() +} + +// Get queries API objects and stores the result in r +func (c *client) Get(r *Resource, labels map[string]string) error { + return api.NewRequest(c.opts). + Get(). + Resource(r.Kind). + Params(&api.Params{LabelSelector: labels}). + Do(). + Into(r.Value) +} + +// Update updates API object +func (c *client) Update(r *Resource) error { + req := api.NewRequest(c.opts). + Patch(). + SetHeader("Content-Type", "application/strategic-merge-patch+json"). + Resource(r.Kind). + Name(r.Name) + + switch r.Kind { + case "service": + req.Body(r.Value.(*Service)) + case "deployment": + req.Body(r.Value.(*Deployment)) + default: + return errors.New("unsupported resource") + } + + return req.Do().Error() +} + +// Delete removes API object +func (c *client) Delete(r *Resource) error { + return api.NewRequest(c.opts). + Delete(). + Resource(r.Kind). + Name(r.Name). + Do(). + Error() +} + +// List lists API objects and stores the result in r +func (c *client) List(r *Resource) error { + labels := map[string]string{ + "micro": "service", + } + return c.Get(r, labels) +} diff --git a/runtime/kubernetes/client/kubernetes.go b/runtime/kubernetes/client/kubernetes.go new file mode 100644 index 00000000..a9d3a653 --- /dev/null +++ b/runtime/kubernetes/client/kubernetes.go @@ -0,0 +1,122 @@ +// Package client provides an implementation of a restricted subset of kubernetes API client +package client + +import ( + "strings" + + "github.com/micro/go-micro/util/log" +) + +var ( + // DefaultImage is default micro image + DefaultImage = "micro/go-micro" +) + +// Kubernetes client +type Kubernetes interface { + // Create creates new API resource + Create(*Resource) error + // Get queries API resrouces + Get(*Resource, map[string]string) error + // Update patches existing API object + Update(*Resource) error + // Delete deletes API resource + Delete(*Resource) error + // List lists API resources + List(*Resource) error +} + +// NewService returns default micro kubernetes service definition +func NewService(name, version, typ string) *Service { + log.Tracef("kubernetes default service: name: %s, version: %s", name, version) + + Labels := map[string]string{ + "name": name, + "version": version, + "micro": typ, + } + + svcName := name + if len(version) > 0 { + // API service object name joins name and version over "-" + svcName = strings.Join([]string{name, version}, "-") + } + + Metadata := &Metadata{ + Name: svcName, + Namespace: "default", + Version: version, + Labels: Labels, + } + + Spec := &ServiceSpec{ + Type: "ClusterIP", + Selector: Labels, + Ports: []ServicePort{{ + "service-port", 9090, "", + }}, + } + + return &Service{ + Metadata: Metadata, + Spec: Spec, + } +} + +// NewService returns default micro kubernetes deployment definition +func NewDeployment(name, version, typ string) *Deployment { + log.Tracef("kubernetes default deployment: name: %s, version: %s", name, version) + + Labels := map[string]string{ + "name": name, + "version": version, + "micro": typ, + } + + depName := name + if len(version) > 0 { + // API deployment object name joins name and version over "-" + depName = strings.Join([]string{name, version}, "-") + } + + Metadata := &Metadata{ + Name: depName, + Namespace: "default", + Version: version, + Labels: Labels, + Annotations: map[string]string{}, + } + + // enable go modules by default + env := EnvVar{ + Name: "GO111MODULE", + Value: "on", + } + + Spec := &DeploymentSpec{ + Replicas: 1, + Selector: &LabelSelector{ + MatchLabels: Labels, + }, + Template: &Template{ + Metadata: Metadata, + PodSpec: &PodSpec{ + Containers: []Container{{ + Name: name, + Image: DefaultImage, + Env: []EnvVar{env}, + Command: []string{"go", "run", "main.go"}, + Ports: []ContainerPort{{ + Name: "service-port", + ContainerPort: 8080, + }}, + }}, + }, + }, + } + + return &Deployment{ + Metadata: Metadata, + Spec: Spec, + } +} diff --git a/runtime/kubernetes/client/templates.go b/runtime/kubernetes/client/templates.go new file mode 100644 index 00000000..e3681539 --- /dev/null +++ b/runtime/kubernetes/client/templates.go @@ -0,0 +1,105 @@ +package client + +var templates = map[string]string{ + "deployment": deploymentTmpl, + "service": serviceTmpl, +} + +var deploymentTmpl = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Metadata.Name }}" + namespace: "{{ .Metadata.Namespace }}" + labels: + {{- with .Metadata.Labels }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} + annotations: + {{- with .Metadata.Annotations }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} +spec: + replicas: {{ .Spec.Replicas }} + selector: + matchLabels: + {{- with .Spec.Selector.MatchLabels }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} + template: + metadata: + labels: + {{- with .Spec.Template.Metadata.Labels }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} + annotations: + {{- with .Spec.Template.Metadata.Annotations }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} + spec: + containers: + {{- with .Spec.Template.PodSpec.Containers }} + {{- range . }} + - name: {{ .Name }} + env: + {{- with .Env }} + {{- range . }} + - name: "{{ .Name }}" + value: "{{ .Value }}" + {{- end }} + {{- end }} + command: + {{- range .Command }} + - {{.}} + {{- end }} + image: {{ .Image }} + imagePullPolicy: Always + ports: + {{- with .Ports }} + {{- range . }} + - containerPort: {{ .ContainerPort }} + name: {{ .Name }} + {{- end}} + {{- end}} + {{- end }} + {{- end}} +` + +var serviceTmpl = ` +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Metadata.Name }}" + namespace: "{{ .Metadata.Namespace }}" + labels: + {{- with .Metadata.Labels }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} +spec: + selector: + {{- with .Spec.Selector }} + {{- range $key, $value := . }} + {{ $key }}: "{{ $value }}" + {{- end }} + {{- end }} + ports: + {{- with .Spec.Ports }} + {{- range . }} + - name: "{{ .Name }}" + port: {{ .Port }} + protocol: {{ .Protocol }} + {{- end }} + {{- end }} +` diff --git a/runtime/kubernetes/client/types.go b/runtime/kubernetes/client/types.go new file mode 100644 index 00000000..b9bfa4a3 --- /dev/null +++ b/runtime/kubernetes/client/types.go @@ -0,0 +1,133 @@ +package client + +// Resource is API resource +type Resource struct { + Name string + Kind string + Value interface{} +} + +// Metadata defines api object metadata +type Metadata struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Version string `json:"version,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +// ServicePort configures service ports +type ServicePort struct { + Name string `json:"name,omitempty"` + Port int `json:"port"` + Protocol string `json:"protocol,omitempty"` +} + +// ServiceSpec provides service configuration +type ServiceSpec struct { + Type string `json:"type,omitempty"` + Selector map[string]string `json:"selector,omitempty"` + Ports []ServicePort `json:"ports,omitempty"` +} + +type LoadBalancerIngress struct { + IP string `json:"ip,omitempty"` + Hostname string `json:"hostname,omitempty"` +} + +type LoadBalancerStatus struct { + Ingress []LoadBalancerIngress `json:"ingress,omitempty"` +} + +// ServiceStatus +type ServiceStatus struct { + LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"` +} + +// Service is kubernetes service +type Service struct { + Metadata *Metadata `json:"metadata"` + Spec *ServiceSpec `json:"spec,omitempty"` + Status *ServiceStatus `json:"status,omitempty"` +} + +// ServiceList +type ServiceList struct { + Items []Service `json:"items"` +} + +// ContainerPort +type ContainerPort struct { + Name string `json:"name,omitempty"` + HostPort int `json:"hostPort,omitempty"` + ContainerPort int `json:"containerPort"` + Protocol string `json:"protocol,omitempty"` +} + +// EnvVar is environment variable +type EnvVar struct { + Name string `json:"name"` + Value string `json:"value,omitempty"` +} + +// Container defined container runtime values +type Container struct { + Name string `json:"name"` + Image string `json:"image"` + Env []EnvVar `json:"env,omitempty"` + Command []string `json:"command,omitempty"` + Ports []ContainerPort `json:"ports,omitempty"` +} + +// PodSpec +type PodSpec struct { + Containers []Container `json:"containers"` +} + +// Template is micro deployment template +type Template struct { + Metadata *Metadata `json:"metadata,omitempty"` + PodSpec *PodSpec `json:"spec,omitempty"` +} + +// LabelSelector is a label query over a set of resources +// NOTE: we do not support MatchExpressions at the moment +type LabelSelector struct { + MatchLabels map[string]string `json:"matchLabels,omitempty"` +} + +// DeploymentSpec defines micro deployment spec +type DeploymentSpec struct { + Replicas int `json:"replicas,omitempty"` + Selector *LabelSelector `json:"selector"` + Template *Template `json:"template,omitempty"` +} + +// DeploymentCondition describes the state of deployment +type DeploymentCondition struct { + Type string `json:"type"` + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` +} + +// DeploymentStatus is returned when querying deployment +type DeploymentStatus struct { + Replicas int `json:"replicas,omitempty"` + UpdatedReplicas int `json:"updatedReplicas,omitempty"` + ReadyReplicas int `json:"readyReplicas,omitempty"` + AvailableReplicas int `json:"availableReplicas,omitempty"` + UnavailableReplicas int `json:"unavailableReplicas,omitempty"` + Conditions []DeploymentCondition `json:"conditions,omitempty"` +} + +// Deployment is Kubernetes deployment +type Deployment struct { + Metadata *Metadata `json:"metadata"` + Spec *DeploymentSpec `json:"spec,omitempty"` + Status *DeploymentStatus `json:"status,omitempty"` +} + +// DeploymentList +type DeploymentList struct { + Items []Deployment `json:"items"` +} diff --git a/runtime/kubernetes/client/util.go b/runtime/kubernetes/client/util.go new file mode 100644 index 00000000..83e17b11 --- /dev/null +++ b/runtime/kubernetes/client/util.go @@ -0,0 +1,102 @@ +package client + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "strings" + "text/template" +) + +// renderTemplateFile renders template for a given resource into writer w +func renderTemplate(resource string, w io.Writer, data interface{}) error { + t := template.Must(template.New("kubernetes").Parse(templates[resource])) + + if err := t.Execute(w, data); err != nil { + return err + } + + return nil +} + +// COPIED FROM +// https://github.com/kubernetes/kubernetes/blob/7a725418af4661067b56506faabc2d44c6d7703a/pkg/util/crypto/crypto.go + +// CertPoolFromFile returns an x509.CertPool containing the certificates in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates +func CertPoolFromFile(filename string) (*x509.CertPool, error) { + certs, err := certificatesFromFile(filename) + if err != nil { + return nil, err + } + pool := x509.NewCertPool() + for _, cert := range certs { + pool.AddCert(cert) + } + return pool, nil +} + +// certificatesFromFile returns the x509.Certificates contained in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates +func certificatesFromFile(file string) ([]*x509.Certificate, error) { + if len(file) == 0 { + return nil, errors.New("error reading certificates from an empty filename") + } + pemBlock, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + certs, err := CertsFromPEM(pemBlock) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", file, err) + } + return certs, nil +} + +// CertsFromPEM returns the x509.Certificates contained in the given PEM-encoded byte array +// Returns an error if a certificate could not be parsed, or if the data does not contain any certificates +func CertsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) { + ok := false + certs := []*x509.Certificate{} + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + // Only use PEM "CERTIFICATE" blocks without extra headers + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, err + } + + certs = append(certs, cert) + ok = true + } + + if !ok { + return certs, errors.New("could not read any certificates") + } + return certs, nil +} + +// Format is used to format a string value into a k8s valid name +func Format(v string) string { + // to lower case + v = strings.ToLower(v) + // dots to dashes + v = strings.ReplaceAll(v, ".", "-") + // limit to 253 chars + if len(v) > 253 { + v = v[:253] + } + // return new name + return v +} diff --git a/runtime/kubernetes/client/util_test.go b/runtime/kubernetes/client/util_test.go new file mode 100644 index 00000000..236f3185 --- /dev/null +++ b/runtime/kubernetes/client/util_test.go @@ -0,0 +1,46 @@ +package client + +import ( + "bytes" + "testing" +) + +func TestTemplates(t *testing.T) { + name := "foo" + version := "123" + typ := "service" + + // Render default service + s := NewService(name, version, typ) + bs := new(bytes.Buffer) + if err := renderTemplate(templates["service"], bs, s); err != nil { + t.Errorf("Failed to render kubernetes service: %v", err) + } + + // Render default deployment + d := NewDeployment(name, version, typ) + bd := new(bytes.Buffer) + if err := renderTemplate(templates["deployment"], bd, d); err != nil { + t.Errorf("Failed to render kubernetes deployment: %v", err) + } +} + +func TestFormatName(t *testing.T) { + testCases := []struct { + name string + expect string + }{ + {"foobar", "foobar"}, + {"foo-bar", "foo-bar"}, + {"foo.bar", "foo-bar"}, + {"Foo.Bar", "foo-bar"}, + {"go.micro.foo.bar", "go-micro-foo-bar"}, + } + + for _, test := range testCases { + v := Format(test.name) + if v != test.expect { + t.Fatalf("Expected name %s for %s got: %s", test.expect, test.name, v) + } + } +} diff --git a/runtime/kubernetes/kubernetes.go b/runtime/kubernetes/kubernetes.go new file mode 100644 index 00000000..d451254a --- /dev/null +++ b/runtime/kubernetes/kubernetes.go @@ -0,0 +1,481 @@ +// Package kubernetes implements kubernetes micro runtime +package kubernetes + +import ( + "fmt" + "sync" + "time" + + "github.com/micro/go-micro/runtime" + "github.com/micro/go-micro/runtime/kubernetes/client" + "github.com/micro/go-micro/util/log" +) + +// action to take on runtime service +type action int + +const ( + start action = iota + update + stop +) + +// task is queued into runtime queue +type task struct { + action action + service *service +} + +type kubernetes struct { + sync.RWMutex + // options configure runtime + options runtime.Options + // indicates if we're running + running bool + // task queue for kubernetes services + queue chan *task + // used to stop the runtime + closed chan bool + // client is kubernetes client + client client.Kubernetes +} + +// getService queries kubernetes for micro service +// NOTE: this function is not thread-safe +func (k *kubernetes) getService(labels map[string]string) ([]*runtime.Service, error) { + // get the service status + serviceList := new(client.ServiceList) + r := &client.Resource{ + Kind: "service", + Value: serviceList, + } + + // get the service from k8s + if err := k.client.Get(r, labels); err != nil { + return nil, err + } + + // get the deployment status + depList := new(client.DeploymentList) + d := &client.Resource{ + Kind: "deployment", + Value: depList, + } + + // get the deployment from k8s + if err := k.client.Get(d, labels); err != nil { + return nil, err + } + + // service map + svcMap := make(map[string]*runtime.Service) + + // collect info from kubernetes service + for _, kservice := range serviceList.Items { + // name of the service + name := kservice.Metadata.Labels["name"] + // version of the service + version := kservice.Metadata.Labels["version"] + + // save as service + svcMap[name+version] = &runtime.Service{ + Name: name, + Version: version, + Metadata: make(map[string]string), + } + + // copy annotations metadata into service metadata + for k, v := range kservice.Metadata.Annotations { + svcMap[name+version].Metadata[k] = v + } + } + + // collect additional info from kubernetes deployment + for _, kdep := range depList.Items { + // name of the service + name := kdep.Metadata.Labels["name"] + // versio of the service + version := kdep.Metadata.Labels["version"] + + // access existing service map based on name + version + if svc, ok := svcMap[name+version]; ok { + // we're expecting our own service name in metadata + if _, ok := kdep.Metadata.Annotations["name"]; !ok { + continue + } + + // set the service name, version and source + // based on existing annotations we stored + svc.Name = kdep.Metadata.Annotations["name"] + svc.Version = kdep.Metadata.Annotations["version"] + svc.Source = kdep.Metadata.Annotations["source"] + + // delete from metadata + delete(kdep.Metadata.Annotations, "name") + delete(kdep.Metadata.Annotations, "version") + delete(kdep.Metadata.Annotations, "source") + + // copy all annotations metadata into service metadata + for k, v := range kdep.Metadata.Annotations { + svc.Metadata[k] = v + } + + // parse out deployment status and inject into service metadata + if len(kdep.Status.Conditions) > 0 { + status := kdep.Status.Conditions[0].Type + // pick the last known condition type and mark the service status with it + log.Debugf("Runtime setting %s service deployment status: %v", name, status) + svc.Metadata["status"] = status + } + + // parse out deployment build + if build, ok := kdep.Spec.Template.Metadata.Annotations["build"]; ok { + buildTime, err := time.Parse(time.RFC3339, build) + if err != nil { + log.Debugf("Runtime failed parsing build time for %s: %v", name, err) + continue + } + svc.Metadata["build"] = fmt.Sprintf("%d", buildTime.Unix()) + continue + } + // if no build annotation is found, set it to current time + svc.Metadata["build"] = fmt.Sprintf("%d", time.Now().Unix()) + } + } + + // collect all the services and return + services := make([]*runtime.Service, 0, len(serviceList.Items)) + + for _, service := range svcMap { + services = append(services, service) + } + + return services, nil +} + +// run runs the runtime management loop +func (k *kubernetes) run(events <-chan runtime.Event) { + t := time.NewTicker(time.Second * 10) + defer t.Stop() + + for { + select { + case <-t.C: + // TODO: figure out what to do here + // - do we even need the ticker for k8s services? + case task := <-k.queue: + // The task queue is used to take actions e.g (CRUD - R) + switch task.action { + case start: + log.Debugf("Runtime starting new service: %s", task.service.Name) + if err := task.service.Start(k.client); err != nil { + log.Debugf("Runtime failed to start service %s: %v", task.service.Name, err) + continue + } + case stop: + log.Debugf("Runtime stopping service: %s", task.service.Name) + if err := task.service.Stop(k.client); err != nil { + log.Debugf("Runtime failed to stop service %s: %v", task.service.Name, err) + continue + } + case update: + log.Debugf("Runtime updating service: %s", task.service.Name) + if err := task.service.Update(k.client); err != nil { + log.Debugf("Runtime failed to update service %s: %v", task.service.Name, err) + continue + } + default: + log.Debugf("Runtime received unknown action for service: %s", task.service.Name) + } + case event := <-events: + // NOTE: we only handle Update events for now + log.Debugf("Runtime received notification event: %v", event) + switch event.Type { + case runtime.Update: + // only process if there's an actual service + // we do not update all the things individually + if len(event.Service) == 0 { + continue + } + + // format the name + name := client.Format(event.Service) + + // set the default labels + labels := map[string]string{ + "micro": k.options.Type, + "name": name, + } + + if len(event.Version) > 0 { + labels["version"] = event.Version + } + + // get the deployment status + deployed := new(client.DeploymentList) + + // get the existing service rather than creating a new one + err := k.client.Get(&client.Resource{ + Kind: "deployment", + Value: deployed, + }, labels) + + if err != nil { + log.Debugf("Runtime update failed to get service %s: %v", event.Service, err) + continue + } + + // technically we should not receive multiple versions but hey ho + for _, service := range deployed.Items { + // check the name matches + if service.Metadata.Name != name { + continue + } + + // update build time annotation + if service.Spec.Template.Metadata.Annotations == nil { + service.Spec.Template.Metadata.Annotations = make(map[string]string) + + } + + // check the existing build timestamp + if build, ok := service.Spec.Template.Metadata.Annotations["build"]; ok { + buildTime, err := time.Parse(time.RFC3339, build) + if err == nil && !event.Timestamp.After(buildTime) { + continue + } + } + + // update the build time + service.Spec.Template.Metadata.Annotations["build"] = event.Timestamp.Format(time.RFC3339) + + log.Debugf("Runtime updating service: %s deployment: %s", event.Service, service.Metadata.Name) + if err := k.client.Update(deploymentResource(&service)); err != nil { + log.Debugf("Runtime failed to update service %s: %v", event.Service, err) + continue + } + } + } + case <-k.closed: + log.Debugf("Runtime stopped") + return + } + } +} + +// Init initializes runtime options +func (k *kubernetes) Init(opts ...runtime.Option) error { + k.Lock() + defer k.Unlock() + + for _, o := range opts { + o(&k.options) + } + + return nil +} + +// Creates a service +func (k *kubernetes) Create(s *runtime.Service, opts ...runtime.CreateOption) error { + k.Lock() + defer k.Unlock() + + options := runtime.CreateOptions{ + Type: k.options.Type, + } + for _, o := range opts { + o(&options) + } + + // quickly prevalidate the name and version + name := s.Name + if len(s.Version) > 0 { + name = name + "-" + s.Version + } + + // format as we'll format in the deployment + name = client.Format(name) + + // create new kubernetes micro service + service := newService(s, options) + + log.Debugf("Runtime queueing service %s version %s for start action", service.Name, service.Version) + + // push into start queue + k.queue <- &task{ + action: start, + service: service, + } + + return nil +} + +// Read returns all instances of given service +func (k *kubernetes) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error) { + k.Lock() + defer k.Unlock() + + // set the default labels + labels := map[string]string{ + "micro": k.options.Type, + } + + var options runtime.ReadOptions + for _, o := range opts { + o(&options) + } + + if len(options.Service) > 0 { + labels["name"] = client.Format(options.Service) + } + + // add version to labels if a version has been supplied + if len(options.Version) > 0 { + labels["version"] = options.Version + } + + if len(options.Type) > 0 { + labels["micro"] = options.Type + } + + return k.getService(labels) +} + +// List the managed services +func (k *kubernetes) List() ([]*runtime.Service, error) { + k.Lock() + defer k.Unlock() + + labels := map[string]string{ + "micro": k.options.Type, + } + + log.Debugf("Runtime listing all micro services") + + return k.getService(labels) +} + +// Update the service in place +func (k *kubernetes) Update(s *runtime.Service) error { + // create new kubernetes micro service + service := newService(s, runtime.CreateOptions{ + Type: k.options.Type, + }) + + // update build time annotation + service.kdeploy.Spec.Template.Metadata.Annotations["build"] = time.Now().Format(time.RFC3339) + + log.Debugf("Runtime queueing service %s for update action", service.Name) + + // queue service for removal + k.queue <- &task{ + action: update, + service: service, + } + + return nil +} + +// Delete removes a service +func (k *kubernetes) Delete(s *runtime.Service) error { + k.Lock() + defer k.Unlock() + + // create new kubernetes micro service + service := newService(s, runtime.CreateOptions{ + Type: k.options.Type, + }) + + log.Debugf("Runtime queueing service %s for delete action", service.Name) + + // queue service for removal + k.queue <- &task{ + action: stop, + service: service, + } + + return nil +} + +// Start starts the runtime +func (k *kubernetes) Start() error { + k.Lock() + defer k.Unlock() + + // already running + if k.running { + return nil + } + + // set running + k.running = true + k.closed = make(chan bool) + + var events <-chan runtime.Event + if k.options.Notifier != nil { + var err error + events, err = k.options.Notifier.Notify() + if err != nil { + // TODO: should we bail here? + log.Debugf("Runtime failed to start update notifier") + } + } + + go k.run(events) + + return nil +} + +// Stop shuts down the runtime +func (k *kubernetes) Stop() error { + k.Lock() + defer k.Unlock() + + if !k.running { + return nil + } + + select { + case <-k.closed: + return nil + default: + close(k.closed) + // set not running + k.running = false + // stop the notifier too + if k.options.Notifier != nil { + return k.options.Notifier.Close() + } + } + + return nil +} + +// String implements stringer interface +func (k *kubernetes) String() string { + return "kubernetes" +} + +// NewRuntime creates new kubernetes runtime +func NewRuntime(opts ...runtime.Option) runtime.Runtime { + // get default options + options := runtime.Options{ + // Create labels with type "micro": "service" + Type: "service", + } + + // apply requested options + for _, o := range opts { + o(&options) + } + + // kubernetes client + client := client.NewClientInCluster() + + return &kubernetes{ + options: options, + closed: make(chan bool), + queue: make(chan *task, 128), + client: client, + } +} diff --git a/runtime/kubernetes/service.go b/runtime/kubernetes/service.go new file mode 100644 index 00000000..ec837682 --- /dev/null +++ b/runtime/kubernetes/service.go @@ -0,0 +1,129 @@ +package kubernetes + +import ( + "strings" + "time" + + "github.com/micro/go-micro/runtime" + "github.com/micro/go-micro/runtime/kubernetes/client" + "github.com/micro/go-micro/util/log" +) + +type service struct { + // service to manage + *runtime.Service + // Kubernetes service + kservice *client.Service + // Kubernetes deployment + kdeploy *client.Deployment +} + +func newService(s *runtime.Service, c runtime.CreateOptions) *service { + // use pre-formatted name/version + name := client.Format(s.Name) + version := client.Format(s.Version) + + kservice := client.NewService(name, version, c.Type) + kdeploy := client.NewDeployment(name, version, c.Type) + + // attach our values to the deployment; name, version, source + kdeploy.Metadata.Annotations["name"] = s.Name + kdeploy.Metadata.Annotations["version"] = s.Version + kdeploy.Metadata.Annotations["source"] = s.Source + + // associate owner:group to be later augmented + kdeploy.Metadata.Annotations["owner"] = "micro" + kdeploy.Metadata.Annotations["group"] = "micro" + + // set a build timestamp to the current time + if kdeploy.Spec.Template.Metadata.Annotations == nil { + kdeploy.Spec.Template.Metadata.Annotations = make(map[string]string) + } + kdeploy.Spec.Template.Metadata.Annotations["build"] = time.Now().Format(time.RFC3339) + + // define the environment values used by the container + env := make([]client.EnvVar, 0, len(c.Env)) + for _, evar := range c.Env { + evarPair := strings.Split(evar, "=") + env = append(env, client.EnvVar{Name: evarPair[0], Value: evarPair[1]}) + } + + // if environment has been supplied update deployment default environment + if len(env) > 0 { + kdeploy.Spec.Template.PodSpec.Containers[0].Env = append(kdeploy.Spec.Template.PodSpec.Containers[0].Env, env...) + } + + // specify the command to exec + if len(c.Command) > 0 { + kdeploy.Spec.Template.PodSpec.Containers[0].Command = c.Command + } else if len(s.Source) > 0 { + // default command for our k8s service should be source + kdeploy.Spec.Template.PodSpec.Containers[0].Command = []string{"go", "run", s.Source} + } + + return &service{ + Service: s, + kservice: kservice, + kdeploy: kdeploy, + } +} + +func deploymentResource(d *client.Deployment) *client.Resource { + return &client.Resource{ + Name: d.Metadata.Name, + Kind: "deployment", + Value: d, + } +} + +func serviceResource(s *client.Service) *client.Resource { + return &client.Resource{ + Name: s.Metadata.Name, + Kind: "service", + Value: s, + } +} + +// Start starts the Kubernetes service. It creates new kubernetes deployment and service API objects +func (s *service) Start(k client.Kubernetes) error { + // create deployment first; if we fail, we dont create service + if err := k.Create(deploymentResource(s.kdeploy)); err != nil { + log.Debugf("Runtime failed to create deployment: %v", err) + return err + } + // create service now that the deployment has been created + if err := k.Create(serviceResource(s.kservice)); err != nil { + log.Debugf("Runtime failed to create service: %v", err) + return err + } + + return nil +} + +func (s *service) Stop(k client.Kubernetes) error { + // first attempt to delete service + if err := k.Delete(serviceResource(s.kservice)); err != nil { + log.Debugf("Runtime failed to delete service: %v", err) + return err + } + // delete deployment once the service has been deleted + if err := k.Delete(deploymentResource(s.kdeploy)); err != nil { + log.Debugf("Runtime failed to delete deployment: %v", err) + return err + } + + return nil +} + +func (s *service) Update(k client.Kubernetes) error { + if err := k.Update(deploymentResource(s.kdeploy)); err != nil { + log.Debugf("Runtime failed to update deployment: %v", err) + return err + } + if err := k.Update(serviceResource(s.kservice)); err != nil { + log.Debugf("Runtime failed to update service: %v", err) + return err + } + + return nil +} diff --git a/runtime/options.go b/runtime/options.go index 64221c9e..e469346b 100644 --- a/runtime/options.go +++ b/runtime/options.go @@ -4,8 +4,35 @@ import ( "io" ) +type Option func(o *Options) + +// Options configure runtime +type Options struct { + // Notifier for updates + Notifier Notifier + // Service type to manage + Type string +} + +// WithNotifier specifies a notifier for updates +func WithNotifier(n Notifier) Option { + return func(o *Options) { + o.Notifier = n + } +} + +// WithType sets the service type to manage +func WithType(t string) Option { + return func(o *Options) { + o.Type = t + } +} + type CreateOption func(o *CreateOptions) +type ReadOption func(o *ReadOptions) + +// CreateOptions configure runtime services type CreateOptions struct { // command to execute including args Command []string @@ -13,19 +40,29 @@ type CreateOptions struct { Env []string // Log output Output io.Writer + // Type of service to create + Type string +} + +// ReadOptions queries runtime services +type ReadOptions struct { + // Service name + Service string + // Version queries services with given version + Version string + // Type of service + Type string } // WithCommand specifies the command to execute -func WithCommand(c string, args ...string) CreateOption { +func WithCommand(args ...string) CreateOption { return func(o *CreateOptions) { // set command - o.Command = []string{c} - // set args - o.Command = append(o.Command, args...) + o.Command = args } } -// WithEnv sets the created service env +// WithEnv sets the created service environment func WithEnv(env []string) CreateOption { return func(o *CreateOptions) { o.Env = env @@ -38,3 +75,24 @@ func WithOutput(out io.Writer) CreateOption { o.Output = out } } + +// ReadService returns services with the given name +func ReadService(service string) ReadOption { + return func(o *ReadOptions) { + o.Service = service + } +} + +// WithVersion confifgures service version +func ReadVersion(version string) ReadOption { + return func(o *ReadOptions) { + o.Version = version + } +} + +// ReadType returns services of the given type +func ReadType(t string) ReadOption { + return func(o *ReadOptions) { + o.Type = t + } +} diff --git a/runtime/process/os/os.go b/runtime/process/os/os.go index ec7d09f3..dda05ab9 100644 --- a/runtime/process/os/os.go +++ b/runtime/process/os/os.go @@ -1,3 +1,5 @@ +// +build !windows + // Package os runs processes locally package os @@ -6,24 +8,25 @@ import ( "os" "os/exec" "strconv" + "syscall" "github.com/micro/go-micro/runtime/process" ) -type Process struct { -} - func (p *Process) Exec(exe *process.Executable) error { - cmd := exec.Command(exe.Binary.Path) + cmd := exec.Command(exe.Package.Path) return cmd.Run() } func (p *Process) Fork(exe *process.Executable) (*process.PID, error) { // create command - cmd := exec.Command(exe.Binary.Path, exe.Args...) + cmd := exec.Command(exe.Package.Path, exe.Args...) // set env vars cmd.Env = append(cmd.Env, exe.Env...) + // create process group + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + in, err := cmd.StdinPipe() if err != nil { return nil, err @@ -61,7 +64,16 @@ func (p *Process) Kill(pid *process.PID) error { return err } - return pr.Kill() + // now kill it + err = pr.Kill() + + // kill the group + if pgid, err := syscall.Getpgid(id); err == nil { + syscall.Kill(-pgid, syscall.SIGKILL) + } + + // return the kill error + return err } func (p *Process) Wait(pid *process.PID) error { @@ -86,7 +98,3 @@ func (p *Process) Wait(pid *process.PID) error { return fmt.Errorf(ps.String()) } - -func NewProcess(opts ...process.Option) process.Process { - return &Process{} -} diff --git a/runtime/process/os/os_windows.go b/runtime/process/os/os_windows.go new file mode 100644 index 00000000..d4420968 --- /dev/null +++ b/runtime/process/os/os_windows.go @@ -0,0 +1,89 @@ +// Package os runs processes locally +package os + +import ( + "fmt" + "os" + "os/exec" + "strconv" + + "github.com/micro/go-micro/runtime/process" +) + +func (p *Process) Exec(exe *process.Executable) error { + cmd := exec.Command(exe.Package.Path) + return cmd.Run() +} + +func (p *Process) Fork(exe *process.Executable) (*process.PID, error) { + // create command + cmd := exec.Command(exe.Package.Path, exe.Args...) + // set env vars + cmd.Env = append(cmd.Env, exe.Env...) + + in, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + out, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + er, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + // start the process + if err := cmd.Start(); err != nil { + return nil, err + } + + return &process.PID{ + ID: fmt.Sprintf("%d", cmd.Process.Pid), + Input: in, + Output: out, + Error: er, + }, nil +} + +func (p *Process) Kill(pid *process.PID) error { + id, err := strconv.Atoi(pid.ID) + if err != nil { + return err + } + + pr, err := os.FindProcess(id) + if err != nil { + return err + } + + // now kill it + err = pr.Kill() + + // return the kill error + return err +} + +func (p *Process) Wait(pid *process.PID) error { + id, err := strconv.Atoi(pid.ID) + if err != nil { + return err + } + + pr, err := os.FindProcess(id) + if err != nil { + return err + } + + ps, err := pr.Wait() + if err != nil { + return err + } + + if ps.Success() { + return nil + } + + return fmt.Errorf(ps.String()) +} diff --git a/runtime/process/os/process.go b/runtime/process/os/process.go new file mode 100644 index 00000000..e756a84c --- /dev/null +++ b/runtime/process/os/process.go @@ -0,0 +1,12 @@ +// Package os runs processes locally +package os + +import ( + "github.com/micro/go-micro/runtime/process" +) + +type Process struct{} + +func NewProcess(opts ...process.Option) process.Process { + return &Process{} +} diff --git a/runtime/process/process.go b/runtime/process/process.go index b5b302d8..8ab78483 100644 --- a/runtime/process/process.go +++ b/runtime/process/process.go @@ -4,7 +4,7 @@ package process import ( "io" - "github.com/micro/go-micro/runtime/package" + "github.com/micro/go-micro/runtime/build" ) // Process manages a running process @@ -20,8 +20,8 @@ type Process interface { } type Executable struct { - // The executable binary - Binary *packager.Binary + // Package containing executable + Package *build.Package // The env variables Env []string // Args to pass diff --git a/runtime/proto/runtime.pb.go b/runtime/proto/runtime.pb.go deleted file mode 100644 index 50f810ab..00000000 --- a/runtime/proto/runtime.pb.go +++ /dev/null @@ -1,366 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: micro/go-micro/runtime/proto/runtime.proto - -package go_micro_runtime - -import ( - context "context" - fmt "fmt" - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" - math "math" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// 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 - -type Service struct { - // name of the service - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // git url of the source - Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` - // local path of the source - Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` - // command to execute - Exec string `protobuf:"bytes,4,opt,name=exec,proto3" json:"exec,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Service) Reset() { *m = Service{} } -func (m *Service) String() string { return proto.CompactTextString(m) } -func (*Service) ProtoMessage() {} -func (*Service) Descriptor() ([]byte, []int) { - return fileDescriptor_efcf18966add108e, []int{0} -} - -func (m *Service) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Service.Unmarshal(m, b) -} -func (m *Service) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Service.Marshal(b, m, deterministic) -} -func (m *Service) XXX_Merge(src proto.Message) { - xxx_messageInfo_Service.Merge(m, src) -} -func (m *Service) XXX_Size() int { - return xxx_messageInfo_Service.Size(m) -} -func (m *Service) XXX_DiscardUnknown() { - xxx_messageInfo_Service.DiscardUnknown(m) -} - -var xxx_messageInfo_Service proto.InternalMessageInfo - -func (m *Service) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Service) GetSource() string { - if m != nil { - return m.Source - } - return "" -} - -func (m *Service) GetPath() string { - if m != nil { - return m.Path - } - return "" -} - -func (m *Service) GetExec() string { - if m != nil { - return m.Exec - } - return "" -} - -type CreateRequest struct { - Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *CreateRequest) Reset() { *m = CreateRequest{} } -func (m *CreateRequest) String() string { return proto.CompactTextString(m) } -func (*CreateRequest) ProtoMessage() {} -func (*CreateRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_efcf18966add108e, []int{1} -} - -func (m *CreateRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CreateRequest.Unmarshal(m, b) -} -func (m *CreateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CreateRequest.Marshal(b, m, deterministic) -} -func (m *CreateRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_CreateRequest.Merge(m, src) -} -func (m *CreateRequest) XXX_Size() int { - return xxx_messageInfo_CreateRequest.Size(m) -} -func (m *CreateRequest) XXX_DiscardUnknown() { - xxx_messageInfo_CreateRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_CreateRequest proto.InternalMessageInfo - -func (m *CreateRequest) GetService() *Service { - if m != nil { - return m.Service - } - return nil -} - -type CreateResponse struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *CreateResponse) Reset() { *m = CreateResponse{} } -func (m *CreateResponse) String() string { return proto.CompactTextString(m) } -func (*CreateResponse) ProtoMessage() {} -func (*CreateResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_efcf18966add108e, []int{2} -} - -func (m *CreateResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_CreateResponse.Unmarshal(m, b) -} -func (m *CreateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_CreateResponse.Marshal(b, m, deterministic) -} -func (m *CreateResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_CreateResponse.Merge(m, src) -} -func (m *CreateResponse) XXX_Size() int { - return xxx_messageInfo_CreateResponse.Size(m) -} -func (m *CreateResponse) XXX_DiscardUnknown() { - xxx_messageInfo_CreateResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_CreateResponse proto.InternalMessageInfo - -type DeleteRequest struct { - Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *DeleteRequest) Reset() { *m = DeleteRequest{} } -func (m *DeleteRequest) String() string { return proto.CompactTextString(m) } -func (*DeleteRequest) ProtoMessage() {} -func (*DeleteRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_efcf18966add108e, []int{3} -} - -func (m *DeleteRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_DeleteRequest.Unmarshal(m, b) -} -func (m *DeleteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_DeleteRequest.Marshal(b, m, deterministic) -} -func (m *DeleteRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_DeleteRequest.Merge(m, src) -} -func (m *DeleteRequest) XXX_Size() int { - return xxx_messageInfo_DeleteRequest.Size(m) -} -func (m *DeleteRequest) XXX_DiscardUnknown() { - xxx_messageInfo_DeleteRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_DeleteRequest proto.InternalMessageInfo - -func (m *DeleteRequest) GetService() *Service { - if m != nil { - return m.Service - } - return nil -} - -type DeleteResponse struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *DeleteResponse) Reset() { *m = DeleteResponse{} } -func (m *DeleteResponse) String() string { return proto.CompactTextString(m) } -func (*DeleteResponse) ProtoMessage() {} -func (*DeleteResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_efcf18966add108e, []int{4} -} - -func (m *DeleteResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_DeleteResponse.Unmarshal(m, b) -} -func (m *DeleteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_DeleteResponse.Marshal(b, m, deterministic) -} -func (m *DeleteResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_DeleteResponse.Merge(m, src) -} -func (m *DeleteResponse) XXX_Size() int { - return xxx_messageInfo_DeleteResponse.Size(m) -} -func (m *DeleteResponse) XXX_DiscardUnknown() { - xxx_messageInfo_DeleteResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_DeleteResponse proto.InternalMessageInfo - -func init() { - proto.RegisterType((*Service)(nil), "go.micro.runtime.Service") - proto.RegisterType((*CreateRequest)(nil), "go.micro.runtime.CreateRequest") - proto.RegisterType((*CreateResponse)(nil), "go.micro.runtime.CreateResponse") - proto.RegisterType((*DeleteRequest)(nil), "go.micro.runtime.DeleteRequest") - proto.RegisterType((*DeleteResponse)(nil), "go.micro.runtime.DeleteResponse") -} - -func init() { - proto.RegisterFile("micro/go-micro/runtime/proto/runtime.proto", fileDescriptor_efcf18966add108e) -} - -var fileDescriptor_efcf18966add108e = []byte{ - // 240 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x91, 0x41, 0x4b, 0xc4, 0x30, - 0x14, 0x84, 0xb7, 0xba, 0xb4, 0xf8, 0x44, 0x59, 0x72, 0x90, 0xe8, 0xc5, 0x25, 0x27, 0x11, 0xcc, - 0x42, 0xfb, 0x13, 0xec, 0xd5, 0x4b, 0x3c, 0x7b, 0xa8, 0xe1, 0x51, 0x0b, 0xb6, 0xa9, 0x49, 0x2a, - 0xfe, 0x23, 0xff, 0xa6, 0x24, 0x2f, 0x15, 0xaa, 0xf5, 0xe4, 0x6d, 0xde, 0x30, 0x7c, 0x33, 0x21, - 0x70, 0xdb, 0x77, 0xda, 0x9a, 0x43, 0x6b, 0xee, 0x48, 0xd8, 0x69, 0xf0, 0x5d, 0x8f, 0x87, 0xd1, - 0x1a, 0xff, 0x7d, 0xc9, 0x78, 0xb1, 0x5d, 0x6b, 0x64, 0x4c, 0xc9, 0xe4, 0x8b, 0x27, 0x28, 0x1e, - 0xd1, 0xbe, 0x77, 0x1a, 0x19, 0x83, 0xed, 0xd0, 0xf4, 0xc8, 0xb3, 0x7d, 0x76, 0x73, 0xa2, 0xa2, - 0x66, 0x17, 0x90, 0x3b, 0x33, 0x59, 0x8d, 0xfc, 0x28, 0xba, 0xe9, 0x0a, 0xd9, 0xb1, 0xf1, 0x2f, - 0xfc, 0x98, 0xb2, 0x41, 0x07, 0x0f, 0x3f, 0x50, 0xf3, 0x2d, 0x79, 0x41, 0x8b, 0x1a, 0xce, 0xee, - 0x2d, 0x36, 0x1e, 0x15, 0xbe, 0x4d, 0xe8, 0x3c, 0xab, 0xa0, 0x70, 0xd4, 0x17, 0x7b, 0x4e, 0xcb, - 0x4b, 0xf9, 0x73, 0x93, 0x4c, 0x83, 0xd4, 0x9c, 0x14, 0x3b, 0x38, 0x9f, 0x29, 0x6e, 0x34, 0x83, - 0xc3, 0xc0, 0xad, 0xf1, 0x15, 0xff, 0xcf, 0x9d, 0x29, 0xc4, 0x2d, 0x3f, 0x33, 0x28, 0x14, 0xc5, - 0xd9, 0x03, 0xe4, 0xd4, 0xca, 0xae, 0x7f, 0xb3, 0x16, 0xaf, 0xba, 0xda, 0xff, 0x1d, 0x48, 0x83, - 0x37, 0x01, 0x47, 0x65, 0x6b, 0xb8, 0xc5, 0x63, 0xd6, 0x70, 0xcb, 0x9d, 0x62, 0xf3, 0x9c, 0xc7, - 0x1f, 0xad, 0xbe, 0x02, 0x00, 0x00, 0xff, 0xff, 0x9a, 0xb6, 0x5a, 0xe7, 0xff, 0x01, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// RuntimeClient is the client API for Runtime service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type RuntimeClient interface { - Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) - Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error) -} - -type runtimeClient struct { - cc *grpc.ClientConn -} - -func NewRuntimeClient(cc *grpc.ClientConn) RuntimeClient { - return &runtimeClient{cc} -} - -func (c *runtimeClient) Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateResponse, error) { - out := new(CreateResponse) - err := c.cc.Invoke(ctx, "/go.micro.runtime.Runtime/Create", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *runtimeClient) Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteResponse, error) { - out := new(DeleteResponse) - err := c.cc.Invoke(ctx, "/go.micro.runtime.Runtime/Delete", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// RuntimeServer is the server API for Runtime service. -type RuntimeServer interface { - Create(context.Context, *CreateRequest) (*CreateResponse, error) - Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) -} - -func RegisterRuntimeServer(s *grpc.Server, srv RuntimeServer) { - s.RegisterService(&_Runtime_serviceDesc, srv) -} - -func _Runtime_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CreateRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(RuntimeServer).Create(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.runtime.Runtime/Create", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RuntimeServer).Create(ctx, req.(*CreateRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Runtime_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(RuntimeServer).Delete(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.runtime.Runtime/Delete", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(RuntimeServer).Delete(ctx, req.(*DeleteRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _Runtime_serviceDesc = grpc.ServiceDesc{ - ServiceName: "go.micro.runtime.Runtime", - HandlerType: (*RuntimeServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Create", - Handler: _Runtime_Create_Handler, - }, - { - MethodName: "Delete", - Handler: _Runtime_Delete_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "micro/go-micro/runtime/proto/runtime.proto", -} diff --git a/runtime/proto/runtime.proto b/runtime/proto/runtime.proto deleted file mode 100644 index 6a3759e3..00000000 --- a/runtime/proto/runtime.proto +++ /dev/null @@ -1,31 +0,0 @@ -syntax = "proto3"; - -package go.micro.runtime; - -service Runtime { - rpc Create(CreateRequest) returns (CreateResponse) {}; - rpc Delete(DeleteRequest) returns (DeleteResponse) {}; -} - -message Service { - // name of the service - string name = 1; - // git url of the source - string source = 2; - // local path of the source - string path = 3; - // command to execute - string exec = 4; -} - -message CreateRequest { - Service service = 1; -} - -message CreateResponse {} - -message DeleteRequest { - Service service = 1; -} - -message DeleteResponse {} diff --git a/runtime/runtime.go b/runtime/runtime.go index 63216867..ef584960 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -1,45 +1,89 @@ // Package runtime is a service runtime manager package runtime +import "time" + +var ( + // DefaultRuntime is default micro runtime + DefaultRuntime Runtime = NewRuntime() + // DefaultName is default runtime service name + DefaultName = "go.micro.runtime" +) + // Runtime is a service runtime manager type Runtime interface { - // Registers a service + // Init initializes runtime + Init(...Option) error + // Create registers a service Create(*Service, ...CreateOption) error + // Read returns the service + Read(...ReadOption) ([]*Service, error) + // Update the service in place + Update(*Service) error // Remove a service Delete(*Service) error - // starts the runtime + // List the managed services + List() ([]*Service, error) + // Start starts the runtime Start() error - // Shutdown the runtime + // Stop shuts down the runtime Stop() error } -type Service struct { - // name of the service - Name string - // url location of source - Source string - // path to store source - Path string - // exec command - Exec string +// Notifier is an update notifier +type Notifier interface { + // Notify publishes notification events + Notify() (<-chan Event, error) + // Close stops the notifier + Close() error } -var ( - DefaultRuntime = newRuntime() +// EventType defines notification event +type EventType int + +const ( + // Create is emitted when a new build has been craeted + Create EventType = iota + // Update is emitted when a new update become available + Update + // Delete is emitted when a build has been deleted + Delete ) -func Create(s *Service, opts ...CreateOption) error { - return DefaultRuntime.Create(s, opts...) +// String returns human readable event type +func (t EventType) String() string { + switch t { + case Create: + return "create" + case Delete: + return "delete" + case Update: + return "update" + default: + return "unknown" + } } -func Delete(s *Service) error { - return DefaultRuntime.Delete(s) +// Event is notification event +type Event struct { + // Type is event type + Type EventType + // Timestamp is event timestamp + Timestamp time.Time + // Service is the name of the service + Service string + // Version of the build + Version string } -func Start() error { - return DefaultRuntime.Start() -} - -func Stop() error { - return DefaultRuntime.Stop() +// Service is runtime service +type Service struct { + // Name of the service + Name string + // Version of the service + Version string + // url location of source + Source string + // Metadata stores metadata + Metadata map[string]string } diff --git a/runtime/service.go b/runtime/service.go new file mode 100644 index 00000000..75e96fbf --- /dev/null +++ b/runtime/service.go @@ -0,0 +1,156 @@ +package runtime + +import ( + "io" + "sync" + "time" + + "github.com/micro/go-micro/runtime/build" + + "github.com/micro/go-micro/runtime/process" + proc "github.com/micro/go-micro/runtime/process/os" + "github.com/micro/go-micro/util/log" +) + +type service struct { + sync.RWMutex + + running bool + closed chan bool + err error + updated time.Time + + // output for logs + output io.Writer + + // service to manage + *Service + // process creator + Process *proc.Process + // Exec + Exec *process.Executable + // process pid + PID *process.PID +} + +func newService(s *Service, c CreateOptions) *service { + var exec string + var args []string + + // set command + exec = c.Command[0] + // set args + if len(c.Command) > 1 { + args = c.Command[1:] + } + + return &service{ + Service: s, + Process: new(proc.Process), + Exec: &process.Executable{ + Package: &build.Package{ + Name: s.Name, + Path: exec, + }, + Env: c.Env, + Args: args, + }, + closed: make(chan bool), + output: c.Output, + updated: time.Now(), + } +} + +func (s *service) streamOutput() { + go io.Copy(s.output, s.PID.Output) + go io.Copy(s.output, s.PID.Error) +} + +// Running returns true is the service is running +func (s *service) Running() bool { + s.RLock() + defer s.RUnlock() + return s.running +} + +// Start stars the service +func (s *service) Start() error { + s.Lock() + defer s.Unlock() + + if s.running { + return nil + } + + // reset + s.err = nil + s.closed = make(chan bool) + + // TODO: pull source & build binary + log.Debugf("Runtime service %s forking new process", s.Service.Name) + p, err := s.Process.Fork(s.Exec) + if err != nil { + return err + } + + // set the pid + s.PID = p + // set to running + s.running = true + + if s.output != nil { + s.streamOutput() + } + + // wait and watch + go s.Wait() + + return nil +} + +// Stop stops the service +func (s *service) Stop() error { + s.Lock() + defer s.Unlock() + + select { + case <-s.closed: + return nil + default: + close(s.closed) + s.running = false + if s.PID == nil { + return nil + } + // kill the process + err := s.Process.Kill(s.PID) + // wait for it to exit + s.Process.Wait(s.PID) + // return the kill error + return err + } +} + +// Error returns the last error service has returned +func (s *service) Error() error { + s.RLock() + defer s.RUnlock() + return s.err +} + +// Wait waits for the service to finish running +func (s *service) Wait() { + // wait for process to exit + err := s.Process.Wait(s.PID) + + s.Lock() + defer s.Unlock() + + // save the error + if err != nil { + s.err = err + } + + // no longer running + s.running = false +} diff --git a/runtime/service/proto/runtime.pb.go b/runtime/service/proto/runtime.pb.go new file mode 100644 index 00000000..9d82b555 --- /dev/null +++ b/runtime/service/proto/runtime.pb.go @@ -0,0 +1,626 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: micro/go-micro/runtime/service/proto/runtime.proto + +package go_micro_runtime + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// 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 + +type Service struct { + // name of the service + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // version of the service + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // git url of the source + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + // service metadata + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Service) Reset() { *m = Service{} } +func (m *Service) String() string { return proto.CompactTextString(m) } +func (*Service) ProtoMessage() {} +func (*Service) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{0} +} + +func (m *Service) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Service.Unmarshal(m, b) +} +func (m *Service) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Service.Marshal(b, m, deterministic) +} +func (m *Service) XXX_Merge(src proto.Message) { + xxx_messageInfo_Service.Merge(m, src) +} +func (m *Service) XXX_Size() int { + return xxx_messageInfo_Service.Size(m) +} +func (m *Service) XXX_DiscardUnknown() { + xxx_messageInfo_Service.DiscardUnknown(m) +} + +var xxx_messageInfo_Service proto.InternalMessageInfo + +func (m *Service) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Service) GetVersion() string { + if m != nil { + return m.Version + } + return "" +} + +func (m *Service) GetSource() string { + if m != nil { + return m.Source + } + return "" +} + +func (m *Service) GetMetadata() map[string]string { + if m != nil { + return m.Metadata + } + return nil +} + +type CreateOptions struct { + // command to pass in + Command []string `protobuf:"bytes,1,rep,name=command,proto3" json:"command,omitempty"` + // environment to pass in + Env []string `protobuf:"bytes,2,rep,name=env,proto3" json:"env,omitempty"` + // output to send to + Output string `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CreateOptions) Reset() { *m = CreateOptions{} } +func (m *CreateOptions) String() string { return proto.CompactTextString(m) } +func (*CreateOptions) ProtoMessage() {} +func (*CreateOptions) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{1} +} + +func (m *CreateOptions) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CreateOptions.Unmarshal(m, b) +} +func (m *CreateOptions) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CreateOptions.Marshal(b, m, deterministic) +} +func (m *CreateOptions) XXX_Merge(src proto.Message) { + xxx_messageInfo_CreateOptions.Merge(m, src) +} +func (m *CreateOptions) XXX_Size() int { + return xxx_messageInfo_CreateOptions.Size(m) +} +func (m *CreateOptions) XXX_DiscardUnknown() { + xxx_messageInfo_CreateOptions.DiscardUnknown(m) +} + +var xxx_messageInfo_CreateOptions proto.InternalMessageInfo + +func (m *CreateOptions) GetCommand() []string { + if m != nil { + return m.Command + } + return nil +} + +func (m *CreateOptions) GetEnv() []string { + if m != nil { + return m.Env + } + return nil +} + +func (m *CreateOptions) GetOutput() string { + if m != nil { + return m.Output + } + return "" +} + +type CreateRequest struct { + Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + Options *CreateOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CreateRequest) Reset() { *m = CreateRequest{} } +func (m *CreateRequest) String() string { return proto.CompactTextString(m) } +func (*CreateRequest) ProtoMessage() {} +func (*CreateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{2} +} + +func (m *CreateRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CreateRequest.Unmarshal(m, b) +} +func (m *CreateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CreateRequest.Marshal(b, m, deterministic) +} +func (m *CreateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CreateRequest.Merge(m, src) +} +func (m *CreateRequest) XXX_Size() int { + return xxx_messageInfo_CreateRequest.Size(m) +} +func (m *CreateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CreateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CreateRequest proto.InternalMessageInfo + +func (m *CreateRequest) GetService() *Service { + if m != nil { + return m.Service + } + return nil +} + +func (m *CreateRequest) GetOptions() *CreateOptions { + if m != nil { + return m.Options + } + return nil +} + +type CreateResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CreateResponse) Reset() { *m = CreateResponse{} } +func (m *CreateResponse) String() string { return proto.CompactTextString(m) } +func (*CreateResponse) ProtoMessage() {} +func (*CreateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{3} +} + +func (m *CreateResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CreateResponse.Unmarshal(m, b) +} +func (m *CreateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CreateResponse.Marshal(b, m, deterministic) +} +func (m *CreateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CreateResponse.Merge(m, src) +} +func (m *CreateResponse) XXX_Size() int { + return xxx_messageInfo_CreateResponse.Size(m) +} +func (m *CreateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CreateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CreateResponse proto.InternalMessageInfo + +type ReadOptions struct { + // service name + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + // version of the service + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // type of service + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReadOptions) Reset() { *m = ReadOptions{} } +func (m *ReadOptions) String() string { return proto.CompactTextString(m) } +func (*ReadOptions) ProtoMessage() {} +func (*ReadOptions) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{4} +} + +func (m *ReadOptions) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReadOptions.Unmarshal(m, b) +} +func (m *ReadOptions) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReadOptions.Marshal(b, m, deterministic) +} +func (m *ReadOptions) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReadOptions.Merge(m, src) +} +func (m *ReadOptions) XXX_Size() int { + return xxx_messageInfo_ReadOptions.Size(m) +} +func (m *ReadOptions) XXX_DiscardUnknown() { + xxx_messageInfo_ReadOptions.DiscardUnknown(m) +} + +var xxx_messageInfo_ReadOptions proto.InternalMessageInfo + +func (m *ReadOptions) GetService() string { + if m != nil { + return m.Service + } + return "" +} + +func (m *ReadOptions) GetVersion() string { + if m != nil { + return m.Version + } + return "" +} + +func (m *ReadOptions) GetType() string { + if m != nil { + return m.Type + } + return "" +} + +type ReadRequest struct { + Options *ReadOptions `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReadRequest) Reset() { *m = ReadRequest{} } +func (m *ReadRequest) String() string { return proto.CompactTextString(m) } +func (*ReadRequest) ProtoMessage() {} +func (*ReadRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{5} +} + +func (m *ReadRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReadRequest.Unmarshal(m, b) +} +func (m *ReadRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReadRequest.Marshal(b, m, deterministic) +} +func (m *ReadRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReadRequest.Merge(m, src) +} +func (m *ReadRequest) XXX_Size() int { + return xxx_messageInfo_ReadRequest.Size(m) +} +func (m *ReadRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ReadRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ReadRequest proto.InternalMessageInfo + +func (m *ReadRequest) GetOptions() *ReadOptions { + if m != nil { + return m.Options + } + return nil +} + +type ReadResponse struct { + Services []*Service `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReadResponse) Reset() { *m = ReadResponse{} } +func (m *ReadResponse) String() string { return proto.CompactTextString(m) } +func (*ReadResponse) ProtoMessage() {} +func (*ReadResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{6} +} + +func (m *ReadResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReadResponse.Unmarshal(m, b) +} +func (m *ReadResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReadResponse.Marshal(b, m, deterministic) +} +func (m *ReadResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReadResponse.Merge(m, src) +} +func (m *ReadResponse) XXX_Size() int { + return xxx_messageInfo_ReadResponse.Size(m) +} +func (m *ReadResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ReadResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ReadResponse proto.InternalMessageInfo + +func (m *ReadResponse) GetServices() []*Service { + if m != nil { + return m.Services + } + return nil +} + +type DeleteRequest struct { + Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DeleteRequest) Reset() { *m = DeleteRequest{} } +func (m *DeleteRequest) String() string { return proto.CompactTextString(m) } +func (*DeleteRequest) ProtoMessage() {} +func (*DeleteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{7} +} + +func (m *DeleteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DeleteRequest.Unmarshal(m, b) +} +func (m *DeleteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DeleteRequest.Marshal(b, m, deterministic) +} +func (m *DeleteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_DeleteRequest.Merge(m, src) +} +func (m *DeleteRequest) XXX_Size() int { + return xxx_messageInfo_DeleteRequest.Size(m) +} +func (m *DeleteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_DeleteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_DeleteRequest proto.InternalMessageInfo + +func (m *DeleteRequest) GetService() *Service { + if m != nil { + return m.Service + } + return nil +} + +type DeleteResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DeleteResponse) Reset() { *m = DeleteResponse{} } +func (m *DeleteResponse) String() string { return proto.CompactTextString(m) } +func (*DeleteResponse) ProtoMessage() {} +func (*DeleteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{8} +} + +func (m *DeleteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DeleteResponse.Unmarshal(m, b) +} +func (m *DeleteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DeleteResponse.Marshal(b, m, deterministic) +} +func (m *DeleteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_DeleteResponse.Merge(m, src) +} +func (m *DeleteResponse) XXX_Size() int { + return xxx_messageInfo_DeleteResponse.Size(m) +} +func (m *DeleteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_DeleteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_DeleteResponse proto.InternalMessageInfo + +type UpdateRequest struct { + Service *Service `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *UpdateRequest) Reset() { *m = UpdateRequest{} } +func (m *UpdateRequest) String() string { return proto.CompactTextString(m) } +func (*UpdateRequest) ProtoMessage() {} +func (*UpdateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{9} +} + +func (m *UpdateRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_UpdateRequest.Unmarshal(m, b) +} +func (m *UpdateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_UpdateRequest.Marshal(b, m, deterministic) +} +func (m *UpdateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_UpdateRequest.Merge(m, src) +} +func (m *UpdateRequest) XXX_Size() int { + return xxx_messageInfo_UpdateRequest.Size(m) +} +func (m *UpdateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_UpdateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_UpdateRequest proto.InternalMessageInfo + +func (m *UpdateRequest) GetService() *Service { + if m != nil { + return m.Service + } + return nil +} + +type UpdateResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *UpdateResponse) Reset() { *m = UpdateResponse{} } +func (m *UpdateResponse) String() string { return proto.CompactTextString(m) } +func (*UpdateResponse) ProtoMessage() {} +func (*UpdateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{10} +} + +func (m *UpdateResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_UpdateResponse.Unmarshal(m, b) +} +func (m *UpdateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_UpdateResponse.Marshal(b, m, deterministic) +} +func (m *UpdateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_UpdateResponse.Merge(m, src) +} +func (m *UpdateResponse) XXX_Size() int { + return xxx_messageInfo_UpdateResponse.Size(m) +} +func (m *UpdateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_UpdateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_UpdateResponse proto.InternalMessageInfo + +type ListRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ListRequest) Reset() { *m = ListRequest{} } +func (m *ListRequest) String() string { return proto.CompactTextString(m) } +func (*ListRequest) ProtoMessage() {} +func (*ListRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{11} +} + +func (m *ListRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ListRequest.Unmarshal(m, b) +} +func (m *ListRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ListRequest.Marshal(b, m, deterministic) +} +func (m *ListRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ListRequest.Merge(m, src) +} +func (m *ListRequest) XXX_Size() int { + return xxx_messageInfo_ListRequest.Size(m) +} +func (m *ListRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ListRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ListRequest proto.InternalMessageInfo + +type ListResponse struct { + Services []*Service `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ListResponse) Reset() { *m = ListResponse{} } +func (m *ListResponse) String() string { return proto.CompactTextString(m) } +func (*ListResponse) ProtoMessage() {} +func (*ListResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_4bc91a8efec81434, []int{12} +} + +func (m *ListResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ListResponse.Unmarshal(m, b) +} +func (m *ListResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ListResponse.Marshal(b, m, deterministic) +} +func (m *ListResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ListResponse.Merge(m, src) +} +func (m *ListResponse) XXX_Size() int { + return xxx_messageInfo_ListResponse.Size(m) +} +func (m *ListResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ListResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ListResponse proto.InternalMessageInfo + +func (m *ListResponse) GetServices() []*Service { + if m != nil { + return m.Services + } + return nil +} + +func init() { + proto.RegisterType((*Service)(nil), "go.micro.runtime.Service") + proto.RegisterMapType((map[string]string)(nil), "go.micro.runtime.Service.MetadataEntry") + proto.RegisterType((*CreateOptions)(nil), "go.micro.runtime.CreateOptions") + proto.RegisterType((*CreateRequest)(nil), "go.micro.runtime.CreateRequest") + proto.RegisterType((*CreateResponse)(nil), "go.micro.runtime.CreateResponse") + proto.RegisterType((*ReadOptions)(nil), "go.micro.runtime.ReadOptions") + proto.RegisterType((*ReadRequest)(nil), "go.micro.runtime.ReadRequest") + proto.RegisterType((*ReadResponse)(nil), "go.micro.runtime.ReadResponse") + proto.RegisterType((*DeleteRequest)(nil), "go.micro.runtime.DeleteRequest") + proto.RegisterType((*DeleteResponse)(nil), "go.micro.runtime.DeleteResponse") + proto.RegisterType((*UpdateRequest)(nil), "go.micro.runtime.UpdateRequest") + proto.RegisterType((*UpdateResponse)(nil), "go.micro.runtime.UpdateResponse") + proto.RegisterType((*ListRequest)(nil), "go.micro.runtime.ListRequest") + proto.RegisterType((*ListResponse)(nil), "go.micro.runtime.ListResponse") +} + +func init() { + proto.RegisterFile("micro/go-micro/runtime/service/proto/runtime.proto", fileDescriptor_4bc91a8efec81434) +} + +var fileDescriptor_4bc91a8efec81434 = []byte{ + // 492 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0xcd, 0x6e, 0xd3, 0x40, + 0x10, 0xae, 0xe3, 0x10, 0xb7, 0x63, 0x8c, 0xa2, 0x15, 0x42, 0xa6, 0x12, 0x10, 0xf9, 0x42, 0x2f, + 0x38, 0x92, 0x2b, 0xc4, 0xdf, 0xb1, 0x29, 0x5c, 0x88, 0x90, 0x5c, 0xf5, 0x01, 0x96, 0x64, 0x54, + 0x59, 0xd4, 0x5e, 0xb3, 0xbb, 0x8e, 0x94, 0x13, 0x57, 0x5e, 0x8f, 0x37, 0x42, 0xfb, 0x97, 0xd8, + 0xa9, 0xdd, 0x4b, 0x6e, 0x33, 0xbb, 0xb3, 0xdf, 0x7c, 0x3f, 0x96, 0x21, 0x2b, 0x8b, 0x15, 0x67, + 0xf3, 0x3b, 0xf6, 0xce, 0x14, 0xbc, 0xa9, 0x64, 0x51, 0xe2, 0x5c, 0x20, 0xdf, 0x14, 0x2b, 0x9c, + 0xd7, 0x9c, 0xc9, 0xdd, 0x69, 0xaa, 0x3b, 0x32, 0xbd, 0x63, 0xa9, 0x9e, 0x4e, 0xed, 0x79, 0xf2, + 0xcf, 0x83, 0xe0, 0xc6, 0xbc, 0x20, 0x04, 0xc6, 0x15, 0x2d, 0x31, 0xf6, 0x66, 0xde, 0xc5, 0x59, + 0xae, 0x6b, 0x12, 0x43, 0xb0, 0x41, 0x2e, 0x0a, 0x56, 0xc5, 0x23, 0x7d, 0xec, 0x5a, 0xf2, 0x02, + 0x26, 0x82, 0x35, 0x7c, 0x85, 0xb1, 0xaf, 0x2f, 0x6c, 0x47, 0xae, 0xe0, 0xb4, 0x44, 0x49, 0xd7, + 0x54, 0xd2, 0x78, 0x3c, 0xf3, 0x2f, 0xc2, 0xec, 0x6d, 0x7a, 0xb8, 0x36, 0xb5, 0x2b, 0xd3, 0xa5, + 0x9d, 0xbc, 0xae, 0x24, 0xdf, 0xe6, 0xbb, 0x87, 0xe7, 0x5f, 0x20, 0xea, 0x5c, 0x91, 0x29, 0xf8, + 0xbf, 0x70, 0x6b, 0xa9, 0xa9, 0x92, 0x3c, 0x87, 0x27, 0x1b, 0x7a, 0xdf, 0xa0, 0xe5, 0x65, 0x9a, + 0xcf, 0xa3, 0x8f, 0x5e, 0x72, 0x03, 0xd1, 0x15, 0x47, 0x2a, 0xf1, 0x47, 0x2d, 0x0b, 0x56, 0x09, + 0x25, 0x62, 0xc5, 0xca, 0x92, 0x56, 0xeb, 0xd8, 0x9b, 0xf9, 0x4a, 0x84, 0x6d, 0x15, 0x2c, 0x56, + 0x9b, 0x78, 0xa4, 0x4f, 0x55, 0xa9, 0x64, 0xb1, 0x46, 0xd6, 0x8d, 0x74, 0xb2, 0x4c, 0x97, 0xfc, + 0x71, 0xa0, 0x39, 0xfe, 0x6e, 0x50, 0x48, 0x72, 0x09, 0x81, 0xb5, 0x5a, 0xb3, 0x0a, 0xb3, 0x97, + 0x83, 0x32, 0x73, 0x37, 0x49, 0x3e, 0x41, 0xc0, 0x0c, 0x29, 0x4d, 0x3b, 0xcc, 0xde, 0x3c, 0x7c, + 0xd4, 0xe1, 0x9e, 0xbb, 0xf9, 0x64, 0x0a, 0xcf, 0x1c, 0x01, 0x51, 0xb3, 0x4a, 0x60, 0x72, 0x0b, + 0x61, 0x8e, 0x74, 0xdd, 0x52, 0xd9, 0x26, 0x74, 0xb6, 0xdf, 0x3a, 0x1c, 0x22, 0x81, 0xb1, 0xdc, + 0xd6, 0x2e, 0x42, 0x5d, 0x27, 0x5f, 0x0d, 0xac, 0xd3, 0xf9, 0x61, 0x4f, 0xd9, 0xe8, 0x7c, 0xf5, + 0x90, 0x72, 0x8b, 0xc6, 0x9e, 0xf0, 0x35, 0x3c, 0x35, 0x38, 0x86, 0x2e, 0x79, 0x0f, 0xa7, 0x96, + 0x90, 0xd0, 0x31, 0x3c, 0xea, 0xd8, 0x6e, 0x34, 0x59, 0x40, 0xb4, 0xc0, 0x7b, 0x3c, 0xce, 0x78, + 0xe5, 0x9e, 0x43, 0xb1, 0xee, 0x2d, 0x20, 0xba, 0xad, 0xd7, 0xf4, 0x78, 0x5c, 0x87, 0x62, 0x71, + 0x23, 0x08, 0xbf, 0x17, 0x42, 0x5a, 0x54, 0xe5, 0x82, 0x69, 0x8f, 0x72, 0x21, 0xfb, 0xeb, 0x43, + 0x90, 0x9b, 0x5b, 0xb2, 0x84, 0x89, 0xf9, 0x12, 0xc8, 0xe0, 0xd7, 0x63, 0xb7, 0x9f, 0xcf, 0x86, + 0x07, 0x2c, 0xdd, 0x13, 0xf2, 0x0d, 0xc6, 0x2a, 0x27, 0x32, 0x90, 0xab, 0x83, 0x7a, 0x3d, 0x74, + 0xbd, 0x03, 0x5a, 0xc2, 0xc4, 0x78, 0xdc, 0xc7, 0xab, 0x93, 0x61, 0x1f, 0xaf, 0x83, 0x78, 0x34, + 0x9c, 0xb1, 0xb6, 0x0f, 0xae, 0x13, 0x5d, 0x1f, 0xdc, 0x41, 0x2a, 0x5a, 0xa6, 0x0a, 0xa2, 0x4f, + 0x66, 0x2b, 0xaf, 0x3e, 0x99, 0xed, 0xfc, 0x92, 0x93, 0x9f, 0x13, 0xfd, 0x2f, 0xbd, 0xfc, 0x1f, + 0x00, 0x00, 0xff, 0xff, 0x14, 0x09, 0x5e, 0x98, 0x81, 0x05, 0x00, 0x00, +} diff --git a/runtime/proto/runtime.micro.go b/runtime/service/proto/runtime.pb.micro.go similarity index 58% rename from runtime/proto/runtime.micro.go rename to runtime/service/proto/runtime.pb.micro.go index f494748f..38f340d4 100644 --- a/runtime/proto/runtime.micro.go +++ b/runtime/service/proto/runtime.pb.micro.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: micro/go-micro/runtime/proto/runtime.proto +// source: micro/go-micro/runtime/service/proto/runtime.proto package go_micro_runtime @@ -35,7 +35,10 @@ var _ server.Option type RuntimeService interface { Create(ctx context.Context, in *CreateRequest, opts ...client.CallOption) (*CreateResponse, error) + Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) + Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) + List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) } type runtimeService struct { @@ -66,6 +69,16 @@ func (c *runtimeService) Create(ctx context.Context, in *CreateRequest, opts ... return out, nil } +func (c *runtimeService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) { + req := c.c.NewRequest(c.name, "Runtime.Read", in) + out := new(ReadResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *runtimeService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) { req := c.c.NewRequest(c.name, "Runtime.Delete", in) out := new(DeleteResponse) @@ -76,17 +89,43 @@ func (c *runtimeService) Delete(ctx context.Context, in *DeleteRequest, opts ... return out, nil } +func (c *runtimeService) Update(ctx context.Context, in *UpdateRequest, opts ...client.CallOption) (*UpdateResponse, error) { + req := c.c.NewRequest(c.name, "Runtime.Update", in) + out := new(UpdateResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (*ListResponse, error) { + req := c.c.NewRequest(c.name, "Runtime.List", in) + out := new(ListResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // Server API for Runtime service type RuntimeHandler interface { Create(context.Context, *CreateRequest, *CreateResponse) error + Read(context.Context, *ReadRequest, *ReadResponse) error Delete(context.Context, *DeleteRequest, *DeleteResponse) error + Update(context.Context, *UpdateRequest, *UpdateResponse) error + List(context.Context, *ListRequest, *ListResponse) error } func RegisterRuntimeHandler(s server.Server, hdlr RuntimeHandler, opts ...server.HandlerOption) error { type runtime interface { Create(ctx context.Context, in *CreateRequest, out *CreateResponse) error + Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error + Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error + List(ctx context.Context, in *ListRequest, out *ListResponse) error } type Runtime struct { runtime @@ -103,6 +142,18 @@ func (h *runtimeHandler) Create(ctx context.Context, in *CreateRequest, out *Cre return h.RuntimeHandler.Create(ctx, in, out) } +func (h *runtimeHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error { + return h.RuntimeHandler.Read(ctx, in, out) +} + func (h *runtimeHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error { return h.RuntimeHandler.Delete(ctx, in, out) } + +func (h *runtimeHandler) Update(ctx context.Context, in *UpdateRequest, out *UpdateResponse) error { + return h.RuntimeHandler.Update(ctx, in, out) +} + +func (h *runtimeHandler) List(ctx context.Context, in *ListRequest, out *ListResponse) error { + return h.RuntimeHandler.List(ctx, in, out) +} diff --git a/runtime/service/proto/runtime.proto b/runtime/service/proto/runtime.proto new file mode 100644 index 00000000..3da3b17d --- /dev/null +++ b/runtime/service/proto/runtime.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package go.micro.runtime; + +service Runtime { + rpc Create(CreateRequest) returns (CreateResponse) {}; + rpc Read(ReadRequest) returns (ReadResponse) {}; + rpc Delete(DeleteRequest) returns (DeleteResponse) {}; + rpc Update(UpdateRequest) returns (UpdateResponse) {}; + rpc List(ListRequest) returns (ListResponse) {}; +} + +message Service { + // name of the service + string name = 1; + // version of the service + string version = 2; + // git url of the source + string source = 3; + // service metadata + map metadata = 4; +} + +message CreateOptions { + // command to pass in + repeated string command = 1; + // environment to pass in + repeated string env = 2; + // output to send to + string output = 3; +} + +message CreateRequest { + Service service = 1; + CreateOptions options = 2; +} + +message CreateResponse {} + +message ReadOptions { + // service name + string service = 1; + // version of the service + string version = 2; + // type of service + string type = 3; +} + +message ReadRequest { + ReadOptions options = 1; +} + +message ReadResponse { + repeated Service services = 1; +} + +message DeleteRequest { + Service service = 1; +} + +message DeleteResponse {} + +message UpdateRequest { + Service service = 1; +} + +message UpdateResponse {} + +message ListRequest {} + +message ListResponse { + repeated Service services = 1; +} diff --git a/runtime/service/service.go b/runtime/service/service.go new file mode 100644 index 00000000..91e62df8 --- /dev/null +++ b/runtime/service/service.go @@ -0,0 +1,185 @@ +package service + +import ( + "context" + "sync" + + "github.com/micro/go-micro/client" + "github.com/micro/go-micro/runtime" + pb "github.com/micro/go-micro/runtime/service/proto" +) + +type svc struct { + sync.RWMutex + options runtime.Options + runtime pb.RuntimeService +} + +// NewRuntime creates new service runtime and returns it +func NewRuntime(opts ...runtime.Option) runtime.Runtime { + // get default options + options := runtime.Options{} + + // apply requested options + for _, o := range opts { + o(&options) + } + + // create default client + cli := client.DefaultClient + + return &svc{ + options: options, + runtime: pb.NewRuntimeService(runtime.DefaultName, cli), + } +} + +// Init initializes runtime with given options +func (s *svc) Init(opts ...runtime.Option) error { + s.Lock() + defer s.Unlock() + + for _, o := range opts { + o(&s.options) + } + + return nil +} + +// Create registers a service in the runtime +func (s *svc) Create(svc *runtime.Service, opts ...runtime.CreateOption) error { + options := runtime.CreateOptions{} + // apply requested options + for _, o := range opts { + o(&options) + } + + // runtime service create request + req := &pb.CreateRequest{ + Service: &pb.Service{ + Name: svc.Name, + Version: svc.Version, + Source: svc.Source, + Metadata: svc.Metadata, + }, + Options: &pb.CreateOptions{ + Command: options.Command, + Env: options.Env, + }, + } + + if _, err := s.runtime.Create(context.Background(), req); err != nil { + return err + } + + return nil +} + +// Read returns the service with the given name from the runtime +func (s *svc) Read(opts ...runtime.ReadOption) ([]*runtime.Service, error) { + options := runtime.ReadOptions{} + // apply requested options + for _, o := range opts { + o(&options) + } + + // runtime service create request + req := &pb.ReadRequest{ + Options: &pb.ReadOptions{ + Service: options.Service, + Version: options.Version, + Type: options.Type, + }, + } + + resp, err := s.runtime.Read(context.Background(), req) + if err != nil { + return nil, err + } + + services := make([]*runtime.Service, 0, len(resp.Services)) + for _, service := range resp.Services { + svc := &runtime.Service{ + Name: service.Name, + Version: service.Version, + Source: service.Source, + Metadata: service.Metadata, + } + services = append(services, svc) + } + + return services, nil +} + +// Update updates the running service +func (s *svc) Update(svc *runtime.Service) error { + // runtime service create request + req := &pb.UpdateRequest{ + Service: &pb.Service{ + Name: svc.Name, + Version: svc.Version, + }, + } + + if _, err := s.runtime.Update(context.Background(), req); err != nil { + return err + } + + return nil +} + +// Delete stops and removes the service from the runtime +func (s *svc) Delete(svc *runtime.Service) error { + // runtime service create request + req := &pb.DeleteRequest{ + Service: &pb.Service{ + Name: svc.Name, + Version: svc.Version, + }, + } + + if _, err := s.runtime.Delete(context.Background(), req); err != nil { + return err + } + + return nil +} + +// List lists all services managed by the runtime +func (s *svc) List() ([]*runtime.Service, error) { + // list all services managed by the runtime + resp, err := s.runtime.List(context.Background(), &pb.ListRequest{}) + if err != nil { + return nil, err + } + + services := make([]*runtime.Service, 0, len(resp.Services)) + for _, service := range resp.Services { + svc := &runtime.Service{ + Name: service.Name, + Version: service.Version, + Source: service.Source, + Metadata: service.Metadata, + } + services = append(services, svc) + } + + return services, nil +} + +// Start starts the runtime +func (s *svc) Start() error { + // NOTE: nothing to be done here + return nil +} + +// Stop stops the runtime +func (s *svc) Stop() error { + // NOTE: nothing to be done here + return nil +} + +// Returns the runtime service implementation +func (s *svc) String() string { + return "service" +} diff --git a/runtime/source/git/git.go b/runtime/source/git/git.go index f8d8000b..deaaa7ef 100644 --- a/runtime/source/git/git.go +++ b/runtime/source/git/git.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/micro/go-micro/runtime/source" - "gopkg.in/src-d/go-git.v4" + git "gopkg.in/src-d/go-git.v4" ) // Source retrieves source code diff --git a/server/extractor.go b/server/extractor.go index 52ce0056..2a9d4e01 100644 --- a/server/extractor.go +++ b/server/extractor.go @@ -95,14 +95,21 @@ func extractEndpoint(method reflect.Method) *registry.Endpoint { request := extractValue(reqType, 0) response := extractValue(rspType, 0) - return ®istry.Endpoint{ + ep := ®istry.Endpoint{ Name: method.Name, Request: request, Response: response, - Metadata: map[string]string{ - "stream": fmt.Sprintf("%v", stream), - }, + Metadata: make(map[string]string), } + + // set endpoint metadata for stream + if stream { + ep.Metadata = map[string]string{ + "stream": fmt.Sprintf("%v", stream), + } + } + + return ep } func extractSubValue(typ reflect.Type) *registry.Value { diff --git a/server/grpc/codec.go b/server/grpc/codec.go index 53de36c3..356f8e81 100644 --- a/server/grpc/codec.go +++ b/server/grpc/codec.go @@ -169,7 +169,7 @@ func (g *grpcCodec) Write(m *codec.Message, v interface{}) error { m.Body = b } // write the body using the framing codec - return g.s.SendMsg(&bytes.Frame{m.Body}) + return g.s.SendMsg(&bytes.Frame{Data: m.Body}) } func (g *grpcCodec) Close() error { diff --git a/server/grpc/extractor.go b/server/grpc/extractor.go index 753cc175..875bb1c9 100644 --- a/server/grpc/extractor.go +++ b/server/grpc/extractor.go @@ -90,14 +90,20 @@ func extractEndpoint(method reflect.Method) *registry.Endpoint { request := extractValue(reqType, 0) response := extractValue(rspType, 0) - return ®istry.Endpoint{ + ep := ®istry.Endpoint{ Name: method.Name, Request: request, Response: response, - Metadata: map[string]string{ - "stream": fmt.Sprintf("%v", stream), - }, + Metadata: make(map[string]string), } + + if stream { + ep.Metadata = map[string]string{ + "stream": fmt.Sprintf("%v", stream), + } + } + + return ep } func extractSubValue(typ reflect.Type) *registry.Value { diff --git a/server/grpc/grpc.go b/server/grpc/grpc.go index 8da88b97..7d3d7426 100644 --- a/server/grpc/grpc.go +++ b/server/grpc/grpc.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "reflect" + "runtime/debug" "sort" "strconv" "strings" @@ -88,6 +89,11 @@ func newGRPCServer(opts ...server.Option) server.Server { type grpcRouter struct { h func(context.Context, server.Request, interface{}) error + m func(context.Context, server.Message) error +} + +func (r grpcRouter) ProcessMessage(ctx context.Context, msg server.Message) error { + return r.m(ctx, msg) } func (r grpcRouter) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error { @@ -172,7 +178,7 @@ func (g *grpcServer) handler(srv interface{}, stream grpc.ServerStream) error { fullMethod, ok := grpc.MethodFromServerStream(stream) if !ok { - return grpc.Errorf(codes.Internal, "method does not exist in context") + return status.Errorf(codes.Internal, "method does not exist in context") } serviceName, methodName, err := mgrpc.ServiceMethod(fullMethod) @@ -216,7 +222,9 @@ func (g *grpcServer) handler(srv interface{}, stream grpc.ServerStream) error { // set the timeout if we have it if len(to) > 0 { if n, err := strconv.ParseUint(to, 10, 64); err == nil { - ctx, _ = context.WithTimeout(ctx, time.Duration(n)) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(n)) + defer cancel() } } @@ -257,7 +265,7 @@ func (g *grpcServer) handler(srv interface{}, stream grpc.ServerStream) error { handler = g.opts.HdlrWrappers[i-1](handler) } - r := grpcRouter{handler} + r := grpcRouter{h: handler} // serve the actual request using the request router if err := r.ServeRequest(ctx, request, response); err != nil { @@ -337,20 +345,22 @@ func (g *grpcServer) processRequest(stream grpc.ServerStream, service *service, } // define the handler func - fn := func(ctx context.Context, req server.Request, rsp interface{}) error { + fn := func(ctx context.Context, req server.Request, rsp interface{}) (err error) { defer func() { if r := recover(); r != nil { - log.Logf("handler %s panic recovered, err: %s", mtype.method.Name, r) + log.Log("panic recovered: ", r) + log.Logf(string(debug.Stack())) + err = errors.InternalServerError("go.micro.server", "panic recovered: %v", r) } }() returnValues = function.Call([]reflect.Value{service.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 { - return err.(error) + if rerr := returnValues[0].Interface(); rerr != nil { + err = rerr.(error) } - return nil + return err } // wrap the handler func @@ -556,7 +566,7 @@ func (g *grpcServer) Register() error { node.Metadata["registry"] = config.Registry.String() node.Metadata["server"] = g.String() node.Metadata["transport"] = g.String() - // node.Metadata["transport"] = config.Transport.String() + node.Metadata["protocol"] = "grpc" g.RLock() // Maps are ordered randomly, sort the keys for consistency @@ -580,7 +590,7 @@ func (g *grpcServer) Register() error { return subscriberList[i].topic > subscriberList[j].topic }) - var endpoints []*registry.Endpoint + endpoints := make([]*registry.Endpoint, 0, len(handlerList)+len(subscriberList)) for _, n := range handlerList { endpoints = append(endpoints, g.handlers[n].Endpoints()...) } @@ -621,7 +631,7 @@ func (g *grpcServer) Register() error { g.registered = true - for sb, _ := range g.subscribers { + for sb := range g.subscribers { handler := g.createSubHandler(sb, g.opts) var opts []broker.SubscribeOption if queue := sb.Options().Queue; len(queue) > 0 { diff --git a/server/grpc/request.go b/server/grpc/request.go index 617b9a7d..adfed48e 100644 --- a/server/grpc/request.go +++ b/server/grpc/request.go @@ -20,6 +20,9 @@ type rpcMessage struct { topic string contentType string payload interface{} + header map[string]string + body []byte + codec codec.Codec } func (r *rpcRequest) ContentType() string { @@ -73,3 +76,15 @@ func (r *rpcMessage) Topic() string { func (r *rpcMessage) Payload() interface{} { return r.payload } + +func (r *rpcMessage) Header() map[string]string { + return r.header +} + +func (r *rpcMessage) Body() []byte { + return r.body +} + +func (r *rpcMessage) Codec() codec.Reader { + return r.codec +} diff --git a/server/grpc/subscriber.go b/server/grpc/subscriber.go index 1f885cab..6d987974 100644 --- a/server/grpc/subscriber.go +++ b/server/grpc/subscriber.go @@ -4,12 +4,15 @@ import ( "context" "fmt" "reflect" + "runtime/debug" "strings" "github.com/micro/go-micro/broker" + "github.com/micro/go-micro/errors" "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/registry" "github.com/micro/go-micro/server" + "github.com/micro/go-micro/util/log" ) const ( @@ -33,7 +36,6 @@ type subscriber struct { } func newSubscriber(topic string, sub interface{}, opts ...server.SubscriberOption) server.Subscriber { - options := server.SubscriberOptions{ AutoAck: true, } @@ -166,6 +168,16 @@ func validateSubscriber(sub server.Subscriber) error { func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broker.Handler { return func(p broker.Event) error { + var err error + + defer func() { + if r := recover(); r != nil { + log.Log("panic recovered: ", r) + log.Logf(string(debug.Stack())) + err = errors.InternalServerError("go.micro.server", "panic recovered: %v", r) + } + }() + msg := p.Message() ct := msg.Header["Content-Type"] if len(ct) == 0 { @@ -202,7 +214,7 @@ func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broke req = req.Elem() } - if err := cf.Unmarshal(msg.Body, req.Interface()); err != nil { + if err = cf.Unmarshal(msg.Body, req.Interface()); err != nil { return err } @@ -218,8 +230,8 @@ func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broke vals = append(vals, reflect.ValueOf(msg.Payload())) returnValues := handler.method.Call(vals) - if err := returnValues[0].Interface(); err != nil { - return err.(error) + if rerr := returnValues[0].Interface(); rerr != nil { + return rerr.(error) } return nil } @@ -239,19 +251,22 @@ func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broke topic: sb.topic, contentType: ct, payload: req.Interface(), + header: msg.Header, + body: msg.Body, }) }() } var errors []string for i := 0; i < len(sb.handlers); i++ { - if err := <-results; err != nil { - errors = append(errors, err.Error()) + if rerr := <-results; err != nil { + errors = append(errors, rerr.Error()) } } if len(errors) > 0 { - return fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n")) + err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n")) } - return nil + + return err } } diff --git a/server/proto/server.pb.go b/server/proto/server.pb.go index e3b8d327..bcd86b46 100644 --- a/server/proto/server.pb.go +++ b/server/proto/server.pb.go @@ -1,13 +1,11 @@ // Code generated by protoc-gen-go. DO NOT EDIT. -// source: micro/go-micro/server/proto/server.proto +// source: github.com/micro/go-micro/server/proto/server.proto package go_micro_server import ( - context "context" fmt "fmt" proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" math "math" ) @@ -35,7 +33,7 @@ func (m *HandleRequest) Reset() { *m = HandleRequest{} } func (m *HandleRequest) String() string { return proto.CompactTextString(m) } func (*HandleRequest) ProtoMessage() {} func (*HandleRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_aabeb7f1c6b4fe84, []int{0} + return fileDescriptor_4cb0c66620400ff8, []int{0} } func (m *HandleRequest) XXX_Unmarshal(b []byte) error { @@ -87,7 +85,7 @@ func (m *HandleResponse) Reset() { *m = HandleResponse{} } func (m *HandleResponse) String() string { return proto.CompactTextString(m) } func (*HandleResponse) ProtoMessage() {} func (*HandleResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_aabeb7f1c6b4fe84, []int{1} + return fileDescriptor_4cb0c66620400ff8, []int{1} } func (m *HandleResponse) XXX_Unmarshal(b []byte) error { @@ -119,7 +117,7 @@ func (m *SubscribeRequest) Reset() { *m = SubscribeRequest{} } func (m *SubscribeRequest) String() string { return proto.CompactTextString(m) } func (*SubscribeRequest) ProtoMessage() {} func (*SubscribeRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_aabeb7f1c6b4fe84, []int{2} + return fileDescriptor_4cb0c66620400ff8, []int{2} } func (m *SubscribeRequest) XXX_Unmarshal(b []byte) error { @@ -157,7 +155,7 @@ func (m *SubscribeResponse) Reset() { *m = SubscribeResponse{} } func (m *SubscribeResponse) String() string { return proto.CompactTextString(m) } func (*SubscribeResponse) ProtoMessage() {} func (*SubscribeResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_aabeb7f1c6b4fe84, []int{3} + return fileDescriptor_4cb0c66620400ff8, []int{3} } func (m *SubscribeResponse) XXX_Unmarshal(b []byte) error { @@ -186,129 +184,24 @@ func init() { } func init() { - proto.RegisterFile("micro/go-micro/server/proto/server.proto", fileDescriptor_aabeb7f1c6b4fe84) + proto.RegisterFile("github.com/micro/go-micro/server/proto/server.proto", fileDescriptor_4cb0c66620400ff8) } -var fileDescriptor_aabeb7f1c6b4fe84 = []byte{ - // 228 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x90, 0xcd, 0x4e, 0xc4, 0x20, - 0x14, 0x85, 0xad, 0xc6, 0xea, 0xdc, 0x44, 0x1d, 0xd1, 0x05, 0xe9, 0xc2, 0x1f, 0x56, 0xdd, 0xc8, - 0x24, 0xfa, 0x12, 0x26, 0xee, 0x66, 0x7c, 0x81, 0x29, 0xbd, 0x69, 0x48, 0x2a, 0x17, 0x81, 0xfa, - 0x52, 0xbe, 0xa4, 0x29, 0x94, 0x46, 0x6b, 0x74, 0xc7, 0xc7, 0x39, 0xdc, 0x73, 0x2e, 0x50, 0xbf, - 0x69, 0xe5, 0x68, 0xd3, 0xd1, 0x43, 0x3a, 0x78, 0x74, 0x1f, 0xe8, 0x36, 0xd6, 0x51, 0xc8, 0x20, - 0x23, 0xb0, 0x8b, 0x8e, 0x64, 0xf4, 0xc8, 0x74, 0x2d, 0xf6, 0x70, 0xf6, 0xbc, 0x37, 0x6d, 0x8f, - 0x5b, 0x7c, 0x1f, 0xd0, 0x07, 0xc6, 0xe1, 0x64, 0x94, 0xb4, 0x42, 0x5e, 0xdc, 0x15, 0xf5, 0x6a, - 0x9b, 0x91, 0x55, 0x70, 0x8a, 0xa6, 0xb5, 0xa4, 0x4d, 0xe0, 0x87, 0x51, 0x9a, 0x79, 0xd4, 0x62, - 0x80, 0xa2, 0x9e, 0x1f, 0x25, 0x2d, 0xb3, 0x58, 0xc3, 0x79, 0x8e, 0xf0, 0x96, 0x8c, 0x47, 0x51, - 0xc3, 0x7a, 0x37, 0x34, 0x5e, 0x39, 0xdd, 0xcc, 0xb9, 0xd7, 0x70, 0x1c, 0xc8, 0x6a, 0x35, 0xa5, - 0x26, 0x10, 0x57, 0x70, 0xf9, 0xcd, 0x99, 0x9e, 0x3f, 0x7e, 0x16, 0x50, 0xee, 0x62, 0x7d, 0xf6, - 0x02, 0x65, 0x9a, 0xcd, 0x6e, 0xe4, 0x62, 0x35, 0xf9, 0x63, 0xaf, 0xea, 0xf6, 0x4f, 0x7d, 0x2a, - 0x75, 0xc0, 0x5e, 0x61, 0x35, 0x87, 0xb1, 0xfb, 0x5f, 0xfe, 0x65, 0xe5, 0x4a, 0xfc, 0x67, 0xc9, - 0x53, 0x9b, 0x32, 0x7e, 0xc4, 0xd3, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf9, 0xb8, 0x5c, 0x69, - 0xa5, 0x01, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// ServerClient is the client API for Server service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type ServerClient interface { - Handle(ctx context.Context, in *HandleRequest, opts ...grpc.CallOption) (*HandleResponse, error) - Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) -} - -type serverClient struct { - cc *grpc.ClientConn -} - -func NewServerClient(cc *grpc.ClientConn) ServerClient { - return &serverClient{cc} -} - -func (c *serverClient) Handle(ctx context.Context, in *HandleRequest, opts ...grpc.CallOption) (*HandleResponse, error) { - out := new(HandleResponse) - err := c.cc.Invoke(ctx, "/go.micro.server.Server/Handle", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *serverClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (*SubscribeResponse, error) { - out := new(SubscribeResponse) - err := c.cc.Invoke(ctx, "/go.micro.server.Server/Subscribe", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// ServerServer is the server API for Server service. -type ServerServer interface { - Handle(context.Context, *HandleRequest) (*HandleResponse, error) - Subscribe(context.Context, *SubscribeRequest) (*SubscribeResponse, error) -} - -func RegisterServerServer(s *grpc.Server, srv ServerServer) { - s.RegisterService(&_Server_serviceDesc, srv) -} - -func _Server_Handle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HandleRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ServerServer).Handle(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.server.Server/Handle", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ServerServer).Handle(ctx, req.(*HandleRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Server_Subscribe_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SubscribeRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ServerServer).Subscribe(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/go.micro.server.Server/Subscribe", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ServerServer).Subscribe(ctx, req.(*SubscribeRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _Server_serviceDesc = grpc.ServiceDesc{ - ServiceName: "go.micro.server.Server", - HandlerType: (*ServerServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Handle", - Handler: _Server_Handle_Handler, - }, - { - MethodName: "Subscribe", - Handler: _Server_Subscribe_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "micro/go-micro/server/proto/server.proto", +var fileDescriptor_4cb0c66620400ff8 = []byte{ + // 236 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x90, 0xcd, 0x4e, 0x03, 0x21, + 0x14, 0x85, 0x1d, 0x8d, 0xa3, 0xbd, 0x89, 0x5a, 0xd1, 0x05, 0x99, 0x85, 0x3f, 0xac, 0xba, 0x91, + 0x49, 0xec, 0x4b, 0x98, 0xb8, 0x6b, 0x7d, 0x81, 0x42, 0x6f, 0x46, 0x92, 0x96, 0x8b, 0xc0, 0xf8, + 0x52, 0xbe, 0xa4, 0x29, 0x94, 0x89, 0x8e, 0xb1, 0x3b, 0x3e, 0xce, 0xe1, 0x9e, 0x73, 0x81, 0x79, + 0x67, 0xe2, 0x7b, 0xaf, 0xa4, 0xa6, 0x6d, 0xbb, 0x35, 0xda, 0x53, 0xdb, 0xd1, 0x53, 0x3e, 0x04, + 0xf4, 0x9f, 0xe8, 0x5b, 0xe7, 0x29, 0x16, 0x90, 0x09, 0xd8, 0x55, 0x47, 0x32, 0x79, 0x64, 0xbe, + 0x16, 0x2b, 0xb8, 0x78, 0x59, 0xd9, 0xf5, 0x06, 0x17, 0xf8, 0xd1, 0x63, 0x88, 0x8c, 0xc3, 0xd9, + 0x4e, 0x32, 0x1a, 0x79, 0xf5, 0x50, 0xcd, 0x26, 0x8b, 0x82, 0xac, 0x81, 0x73, 0xb4, 0x6b, 0x47, + 0xc6, 0x46, 0x7e, 0x9c, 0xa4, 0x81, 0x77, 0x5a, 0x0a, 0xd0, 0xb4, 0xe1, 0x27, 0x59, 0x2b, 0x2c, + 0xa6, 0x70, 0x59, 0x22, 0x82, 0x23, 0x1b, 0x50, 0xcc, 0x60, 0xba, 0xec, 0x55, 0xd0, 0xde, 0xa8, + 0x21, 0xf7, 0x16, 0x4e, 0x23, 0x39, 0xa3, 0xf7, 0xa9, 0x19, 0xc4, 0x0d, 0x5c, 0xff, 0x70, 0xe6, + 0xe7, 0xcf, 0x5f, 0x15, 0xd4, 0xcb, 0x54, 0x9f, 0xbd, 0x42, 0x9d, 0x67, 0xb3, 0x3b, 0x39, 0x5a, + 0x4d, 0xfe, 0xda, 0xab, 0xb9, 0xff, 0x57, 0xdf, 0x97, 0x3a, 0x62, 0x6f, 0x30, 0x19, 0xc2, 0xd8, + 0xe3, 0x1f, 0xff, 0xb8, 0x72, 0x23, 0x0e, 0x59, 0xca, 0x54, 0x55, 0xa7, 0x8f, 0x98, 0x7f, 0x07, + 0x00, 0x00, 0xff, 0xff, 0x30, 0xd3, 0x7a, 0x91, 0xb0, 0x01, 0x00, 0x00, } diff --git a/server/proto/server.micro.go b/server/proto/server.pb.micro.go similarity index 97% rename from server/proto/server.micro.go rename to server/proto/server.pb.micro.go index 2f95eaf6..77194020 100644 --- a/server/proto/server.micro.go +++ b/server/proto/server.pb.micro.go @@ -1,5 +1,5 @@ // Code generated by protoc-gen-micro. DO NOT EDIT. -// source: micro/go-micro/server/proto/server.proto +// source: github.com/micro/go-micro/server/proto/server.proto package go_micro_server diff --git a/server/rpc_codec.go b/server/rpc_codec.go index 2fea1b83..53d552f4 100644 --- a/server/rpc_codec.go +++ b/server/rpc_codec.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "sync" "github.com/micro/go-micro/codec" raw "github.com/micro/go-micro/codec/bytes" @@ -11,20 +12,25 @@ import ( "github.com/micro/go-micro/codec/proto" "github.com/micro/go-micro/codec/protorpc" "github.com/micro/go-micro/transport" + "github.com/oxtoacart/bpool" "github.com/pkg/errors" ) type rpcCodec struct { socket transport.Socket codec codec.Codec - first bool protocol string req *transport.Message buf *readWriteCloser + + // check if we're the first + sync.RWMutex + first chan bool } type readWriteCloser struct { + sync.RWMutex wbuf *bytes.Buffer rbuf *bytes.Buffer } @@ -51,19 +57,24 @@ var ( "application/proto-rpc": protorpc.NewCodec, "application/octet-stream": protorpc.NewCodec, } + + // the local buffer pool + bufferPool = bpool.NewSizedBufferPool(32, 1) ) func (rwc *readWriteCloser) Read(p []byte) (n int, err error) { + rwc.RLock() + defer rwc.RUnlock() return rwc.rbuf.Read(p) } func (rwc *readWriteCloser) Write(p []byte) (n int, err error) { + rwc.Lock() + defer rwc.Unlock() return rwc.wbuf.Write(p) } func (rwc *readWriteCloser) Close() error { - rwc.rbuf.Reset() - rwc.wbuf.Reset() return nil } @@ -75,24 +86,18 @@ func getHeader(hdr string, md map[string]string) string { } func getHeaders(m *codec.Message) { - get := func(hdr, v string) string { + set := func(v, hdr string) string { if len(v) > 0 { return v } - - if hd := m.Header[hdr]; len(hd) > 0 { - return hd - } - - // old - return m.Header["X-"+hdr] + return m.Header[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) + m.Id = set(m.Id, "Micro-Id") + m.Error = set(m.Error, "Micro-Error") + m.Endpoint = set(m.Endpoint, "Micro-Endpoint") + m.Method = set(m.Method, "Micro-Method") + m.Target = set(m.Target, "Micro-Service") // TODO: remove this cruft if len(m.Endpoint) == 0 { @@ -124,12 +129,18 @@ func setupProtocol(msg *transport.Message) codec.NewCodec { endpoint := getHeader("Micro-Endpoint", msg.Header) protocol := getHeader("Micro-Protocol", msg.Header) target := getHeader("Micro-Target", msg.Header) + topic := getHeader("Micro-Topic", msg.Header) // if the protocol exists (mucp) do nothing if len(protocol) > 0 { return nil } + // newer method of processing messages over transport + if len(topic) > 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"]] @@ -155,8 +166,8 @@ func setupProtocol(msg *transport.Message) codec.NewCodec { func newRpcCodec(req *transport.Message, socket transport.Socket, c codec.NewCodec) codec.Codec { rwc := &readWriteCloser{ - rbuf: bytes.NewBuffer(nil), - wbuf: bytes.NewBuffer(nil), + rbuf: bufferPool.Get(), + wbuf: bufferPool.Get(), } r := &rpcCodec{ @@ -165,18 +176,20 @@ func newRpcCodec(req *transport.Message, socket transport.Socket, c codec.NewCod req: req, socket: socket, protocol: "mucp", + first: make(chan bool), } // if grpc pre-load the buffer // TODO: remove this terrible hack switch r.codec.String() { case "grpc": - // set as first - r.first = true // write the body rwc.rbuf.Write(req.Body) // set the protocol r.protocol = "grpc" + default: + // first is not preloaded + close(r.first) } return r @@ -190,7 +203,9 @@ func (c *rpcCodec) ReadHeader(r *codec.Message, t codec.MessageType) error { } // first message could be pre-loaded - if !c.first { + select { + case <-c.first: + // not the first var tm transport.Message // read off the socket @@ -212,11 +227,26 @@ func (c *rpcCodec) ReadHeader(r *codec.Message, t codec.MessageType) error { // set req c.req = &tm + default: + // we need to lock here to prevent race conditions + // and we make use of a channel otherwise because + // this does not result in a context switch + // locking to check c.first on every call to ReadHeader + // would otherwise drastically slow the code execution + c.Lock() + // recheck before closing because the select statement + // above is not thread safe, so thread safety here is + // mandatory + select { + case <-c.first: + default: + // disable first + close(c.first) + } + // now unlock and we never need this again + c.Unlock() } - // disable first - c.first = false - // set some internal things getHeaders(&m) @@ -285,7 +315,6 @@ func (c *rpcCodec) Write(r *codec.Message, b interface{}) error { // 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 { @@ -309,9 +338,15 @@ func (c *rpcCodec) Write(r *codec.Message, b interface{}) error { } func (c *rpcCodec) Close() error { - c.buf.Close() + // close the codec c.codec.Close() - return c.socket.Close() + // close the socket + err := c.socket.Close() + // put back the buffers + bufferPool.Put(c.buf.rbuf) + bufferPool.Put(c.buf.wbuf) + // return the error + return err } func (c *rpcCodec) String() string { diff --git a/server/rpc_event.go b/server/rpc_event.go new file mode 100644 index 00000000..4bd39d4d --- /dev/null +++ b/server/rpc_event.go @@ -0,0 +1,33 @@ +package server + +import ( + "github.com/micro/go-micro/broker" + "github.com/micro/go-micro/transport" +) + +// event is a broker event we handle on the server transport +type event struct { + message *broker.Message +} + +func (e *event) Ack() error { + // there is no ack support + return nil +} + +func (e *event) Message() *broker.Message { + return e.message +} + +func (e *event) Topic() string { + return e.message.Header["Micro-Topic"] +} + +func newEvent(msg transport.Message) *event { + return &event{ + message: &broker.Message{ + Header: msg.Header, + Body: msg.Body, + }, + } +} diff --git a/server/rpc_request.go b/server/rpc_request.go index 532cf4b1..065d57e4 100644 --- a/server/rpc_request.go +++ b/server/rpc_request.go @@ -1,8 +1,11 @@ package server import ( + "bytes" + "github.com/micro/go-micro/codec" "github.com/micro/go-micro/transport" + "github.com/micro/go-micro/util/buf" ) type rpcRequest struct { @@ -23,6 +26,9 @@ type rpcMessage struct { topic string contentType string payload interface{} + header map[string]string + body []byte + codec codec.NewCodec } func (r *rpcRequest) Codec() codec.Reader { @@ -86,3 +92,16 @@ func (r *rpcMessage) Topic() string { func (r *rpcMessage) Payload() interface{} { return r.payload } + +func (r *rpcMessage) Header() map[string]string { + return r.header +} + +func (r *rpcMessage) Body() []byte { + return r.body +} + +func (r *rpcMessage) Codec() codec.Reader { + b := buf.New(bytes.NewBuffer(r.body)) + return r.codec(b) +} diff --git a/server/rpc_router.go b/server/rpc_router.go index b1eb66ff..19f672f4 100644 --- a/server/rpc_router.go +++ b/server/rpc_router.go @@ -9,14 +9,17 @@ package server import ( "context" "errors" + "fmt" "io" "reflect" + "runtime/debug" "strings" "sync" "unicode" "unicode/utf8" "github.com/micro/go-micro/codec" + merrors "github.com/micro/go-micro/errors" "github.com/micro/go-micro/util/log" ) @@ -60,19 +63,44 @@ type response struct { // router represents an RPC router. type router struct { - name string - mu sync.Mutex // protects the serviceMap - serviceMap map[string]*service - reqLock sync.Mutex // protects freeReq - freeReq *request - respLock sync.Mutex // protects freeResp - freeResp *response + name string + + mu sync.Mutex // protects the serviceMap + serviceMap map[string]*service + + reqLock sync.Mutex // protects freeReq + freeReq *request + + respLock sync.Mutex // protects freeResp + freeResp *response + + // handler wrappers hdlrWrappers []HandlerWrapper + // subscriber wrappers + subWrappers []SubscriberWrapper + + su sync.RWMutex + subscribers map[string][]*subscriber +} + +// rpcRouter encapsulates functions that become a server.Router +type rpcRouter struct { + h func(context.Context, Request, interface{}) error + m func(context.Context, Message) error +} + +func (r rpcRouter) ProcessMessage(ctx context.Context, msg Message) error { + return r.m(ctx, msg) +} + +func (r rpcRouter) ServeRequest(ctx context.Context, req Request, rsp Response) error { + return r.h(ctx, req, rsp) } func newRpcRouter() *router { return &router{ - serviceMap: make(map[string]*service), + serviceMap: make(map[string]*service), + subscribers: make(map[string][]*subscriber), } } @@ -449,3 +477,155 @@ func (router *router) ServeRequest(ctx context.Context, r Request, rsp Response) } return service.call(ctx, router, sending, mtype, req, argv, replyv, rsp.Codec()) } + +func (router *router) NewSubscriber(topic string, handler interface{}, opts ...SubscriberOption) Subscriber { + return newSubscriber(topic, handler, opts...) +} + +func (router *router) Subscribe(s Subscriber) error { + sub, ok := s.(*subscriber) + if !ok { + return fmt.Errorf("invalid subscriber: expected *subscriber") + } + if len(sub.handlers) == 0 { + return fmt.Errorf("invalid subscriber: no handler functions") + } + + if err := validateSubscriber(sub); err != nil { + return err + } + + router.su.Lock() + defer router.su.Unlock() + + // append to subscribers + subs := router.subscribers[sub.Topic()] + subs = append(subs, sub) + router.subscribers[sub.Topic()] = subs + + return nil +} + +func (router *router) ProcessMessage(ctx context.Context, msg Message) error { + var err error + + defer func() { + // recover any panics + if r := recover(); r != nil { + log.Log("panic recovered: ", r) + log.Log(string(debug.Stack())) + err = merrors.InternalServerError("go.micro.server", "panic recovered: %v", r) + } + }() + + router.su.RLock() + + // get the subscribers by topic + subs, ok := router.subscribers[msg.Topic()] + if !ok { + router.su.RUnlock() + return nil + } + + // unlock since we only need to get the subs + router.su.RUnlock() + + var errResults []string + + // we may have multiple subscribers for the topic + for _, sub := range subs { + // we may have multiple handlers per subscriber + for i := 0; i < len(sub.handlers); i++ { + // get the handler + handler := sub.handlers[i] + + var isVal bool + var req reflect.Value + + // check whether the handler is a pointer + if handler.reqType.Kind() == reflect.Ptr { + req = reflect.New(handler.reqType.Elem()) + } else { + req = reflect.New(handler.reqType) + isVal = true + } + + // if its a value get the element + if isVal { + req = req.Elem() + } + + if handler.reqType.Kind() == reflect.Ptr { + req = reflect.New(handler.reqType.Elem()) + } else { + req = reflect.New(handler.reqType) + isVal = true + } + + // if its a value get the element + if isVal { + req = req.Elem() + } + + cc := msg.Codec() + + // read the header. mostly a noop + if err = cc.ReadHeader(&codec.Message{}, codec.Event); err != nil { + return err + } + + // read the body into the handler request value + if err = cc.ReadBody(req.Interface()); err != nil { + return err + } + + // create the handler which will honour the SubscriberFunc type + fn := func(ctx context.Context, msg Message) error { + var vals []reflect.Value + if sub.typ.Kind() != reflect.Func { + vals = append(vals, sub.rcvr) + } + if handler.ctxType != nil { + vals = append(vals, reflect.ValueOf(ctx)) + } + + // values to pass the handler + vals = append(vals, reflect.ValueOf(msg.Payload())) + + // execute the actuall call of the handler + returnValues := handler.method.Call(vals) + if rerr := returnValues[0].Interface(); rerr != nil { + err = rerr.(error) + } + return err + } + + // wrap with subscriber wrappers + for i := len(router.subWrappers); i > 0; i-- { + fn = router.subWrappers[i-1](fn) + } + + // create new rpc message + rpcMsg := &rpcMessage{ + topic: msg.Topic(), + contentType: msg.ContentType(), + payload: req.Interface(), + codec: msg.(*rpcMessage).codec, + header: msg.Header(), + body: msg.Body(), + } + + // execute the message handler + if err = fn(ctx, rpcMsg); err != nil { + errResults = append(errResults, err.Error()) + } + } + } + + // if no errors just return + if len(errResults) > 0 { + err = merrors.InternalServerError("go.micro.server", "subscriber error: %v", strings.Join(errResults, "\n")) + } + + return err +} diff --git a/server/rpc_server.go b/server/rpc_server.go index 89d60f9e..d23401c8 100644 --- a/server/rpc_server.go +++ b/server/rpc_server.go @@ -14,6 +14,7 @@ import ( "github.com/micro/go-micro/broker" "github.com/micro/go-micro/codec" + raw "github.com/micro/go-micro/codec/bytes" "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/registry" "github.com/micro/go-micro/transport" @@ -30,11 +31,13 @@ type rpcServer struct { sync.RWMutex opts Options handlers map[string]Handler - subscribers map[*subscriber][]broker.Subscriber + subscribers map[Subscriber][]broker.Subscriber // marks the serve as started started bool // used for first registration registered bool + // subscribe to service name + subscriber broker.Subscriber // graceful exit wg *sync.WaitGroup } @@ -43,47 +46,116 @@ func newRpcServer(opts ...Option) Server { options := newOptions(opts...) router := newRpcRouter() router.hdlrWrappers = options.HdlrWrappers + router.subWrappers = options.SubWrappers return &rpcServer{ opts: options, router: router, handlers: make(map[string]Handler), - subscribers: make(map[*subscriber][]broker.Subscriber), + subscribers: make(map[Subscriber][]broker.Subscriber), exit: make(chan chan error), wg: wait(options.Context), } } -type rpcRouter struct { - h func(context.Context, Request, interface{}) error -} +// HandleEvent handles inbound messages to the service directly +// TODO: handle requests from an event. We won't send a response. +func (s *rpcServer) HandleEvent(e broker.Event) error { + // formatting horrible cruft + msg := e.Message() -func (r rpcRouter) ServeRequest(ctx context.Context, req Request, rsp Response) error { - return r.h(ctx, req, rsp) + if msg.Header == nil { + // create empty map in case of headers empty to avoid panic later + msg.Header = make(map[string]string) + } + + // 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 + } + + // create context + ctx := metadata.NewContext(context.Background(), hdr) + + // TODO: inspect message header + // Micro-Service means a request + // Micro-Topic means a message + + rpcMsg := &rpcMessage{ + topic: msg.Header["Micro-Topic"], + contentType: ct, + payload: &raw.Frame{Data: msg.Body}, + codec: cf, + header: msg.Header, + body: msg.Body, + } + + // existing router + r := Router(s.router) + + // if the router is present then execute it + if s.opts.Router != nil { + // create a wrapped function + handler := s.opts.Router.ProcessMessage + + // execute the wrapper for it + for i := len(s.opts.SubWrappers); i > 0; i-- { + handler = s.opts.SubWrappers[i-1](handler) + } + + // set the router + r = rpcRouter{m: handler} + } + + return r.ProcessMessage(ctx, rpcMsg) } // ServeConn serves a single connection func (s *rpcServer) ServeConn(sock transport.Socket) { - var wg sync.WaitGroup - var mtx sync.RWMutex + // global error tracking + var gerr error // streams are multiplexed on Micro-Stream or Micro-Id header - sockets := make(map[string]*socket.Socket) + pool := socket.NewPool() + + // get global waitgroup + s.Lock() + gg := s.wg + s.Unlock() + + // waitgroup to wait for processing to finish + wg := &waitGroup{ + gg: gg, + } defer func() { - // wait till done - wg.Wait() + // only wait if there's no error + if gerr == nil { + // wait till done + wg.Wait() + } + + // close all the sockets for this connection + pool.Close() // close underlying socket sock.Close() - // close the sockets - mtx.Lock() - for id, psock := range sockets { - psock.Close() - delete(sockets, id) - } - mtx.Unlock() - // recover any panics if r := recover(); r != nil { log.Log("panic recovered: ", r) @@ -93,10 +165,38 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { for { var msg transport.Message + // process inbound messages one at a time if err := sock.Recv(&msg); err != nil { + // set a global error and return + // we're saying we essentially can't + // use the socket anymore + gerr = err return } + // check the message header for + // Micro-Service is a request + // Micro-Topic is a message + if t := msg.Header["Micro-Topic"]; len(t) > 0 { + // process the event + ev := newEvent(msg) + // TODO: handle the error event + if err := s.HandleEvent(ev); err != nil { + msg.Header["Micro-Error"] = err.Error() + } + // write back some 200 + if err := sock.Send(&transport.Message{ + Header: msg.Header, + }); err != nil { + gerr = err + break + } + // we're done + continue + } + + // business as usual + // use Micro-Stream as the stream identifier // in the event its blank we'll always process // on the same socket @@ -108,53 +208,52 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { id = msg.Header["Micro-Id"] } - // we're starting processing - wg.Add(1) + // check stream id + var stream bool - // add to wait group if "wait" is opt-in - if s.wg != nil { - s.wg.Add(1) + if v := getHeader("Micro-Stream", msg.Header); len(v) > 0 { + stream = true } - // check we have an existing socket - mtx.RLock() - psock, ok := sockets[id] - mtx.RUnlock() + // check if we have an existing socket + psock, ok := pool.Get(id) - // got the socket + // if we don't have a socket and its a stream + if !ok && stream { + // check if its a last stream EOS error + err := msg.Header["Micro-Error"] + if err == lastStreamResponseError.Error() { + pool.Release(psock) + continue + } + } + + // got an existing socket already if ok { - // accept the message + // we're starting processing + wg.Add(1) + + // pass the message to that existing socket if err := psock.Accept(&msg); err != nil { - // delete the socket - mtx.Lock() - delete(sockets, id) - mtx.Unlock() - } - - // done(1) - if s.wg != nil { - s.wg.Done() + // release the socket if there's an error + pool.Release(psock) } + // done waiting wg.Done() // continue to the next message continue } - // no socket was found - psock = socket.New() + // no socket was found so its new + // set the local and remote values psock.SetLocal(sock.Local()) psock.SetRemote(sock.Remote()) - // load the socket + // load the socket with the current message psock.Accept(&msg) - // save a new socket - mtx.Lock() - sockets[id] = psock - mtx.Unlock() - // now walk the usual path // we use this Timeout header to set a server deadline @@ -178,7 +277,9 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { // set the timeout from the header if we have it if len(to) > 0 { if n, err := strconv.ParseUint(to, 10, 64); err == nil { - ctx, _ = context.WithTimeout(ctx, time.Duration(n)) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(n)) + defer cancel() } } @@ -191,38 +292,33 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { // setup old protocol cf := setupProtocol(&msg) - // no old codec + // no legacy codec needed if cf == nil { - // TODO: needs better error handling var err error + // try get a new codec if cf, err = s.newCodec(ct); err != nil { - sock.Send(&transport.Message{ + // no codec found so send back an error + if err := sock.Send(&transport.Message{ Header: map[string]string{ "Content-Type": "text/plain", }, Body: []byte(err.Error()), - }) - - if s.wg != nil { - s.wg.Done() + }); err != nil { + gerr = err } - wg.Done() - - return + // release the socket we just created + pool.Release(psock) + // now continue + continue } } + // create a new rpc codec based on the pseudo socket and codec rcodec := newRpcCodec(&msg, psock, cf) + // check the protocol as well protocol := rcodec.String() - // check stream id - var stream bool - - if v := getHeader("Micro-Stream", msg.Header); len(v) > 0 { - stream = true - } - // internal request request := &rpcRequest{ service: getHeader("Micro-Service", msg.Header), @@ -259,14 +355,14 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { } // set the router - r = rpcRouter{handler} + r = rpcRouter{h: handler} } - // wait for processing to exit - wg.Add(1) - // process the outbound messages from the socket go func(id string, psock *socket.Socket) { + // wait for processing to exit + wg.Add(1) + defer func() { // TODO: don't hack this but if its grpc just break out of the stream // We do this because the underlying connection is h2 and its a stream @@ -274,7 +370,9 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { case "grpc": sock.Close() } - + // release the socket + pool.Release(psock) + // signal we're done wg.Done() }() @@ -282,10 +380,6 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { // get the message from our internal handler/stream m := new(transport.Message) if err := psock.Process(m); err != nil { - // delete the socket - mtx.Lock() - delete(sockets, id) - mtx.Unlock() return } @@ -298,7 +392,15 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { // serve the request in a go routine as this may be a stream go func(id string, psock *socket.Socket) { - defer psock.Close() + // add to the waitgroup + wg.Add(1) + + defer func() { + // release the socket + pool.Release(psock) + // signal we're done + wg.Done() + }() // serve the actual request using the request router if serveRequestError := r.ServeRequest(ctx, request, response); serveRequestError != nil { @@ -315,21 +417,9 @@ func (s *rpcServer) ServeConn(sock transport.Socket) { // could not write error response if writeError != nil && !alreadyClosed { - log.Logf("rpc: unable to write error response: %v", writeError) + log.Debugf("rpc: unable to write error response: %v", writeError) } } - - mtx.Lock() - delete(sockets, id) - mtx.Unlock() - - // signal we're done - if s.wg != nil { - s.wg.Done() - } - - // done with this socket - wg.Done() }(id, psock) } } @@ -362,6 +452,7 @@ func (s *rpcServer) Init(opts ...Option) error { r := newRpcRouter() r.hdlrWrappers = s.opts.HdlrWrappers r.serviceMap = s.router.serviceMap + r.subWrappers = s.opts.SubWrappers s.router = r } @@ -387,29 +478,18 @@ func (s *rpcServer) Handle(h Handler) error { } func (s *rpcServer) NewSubscriber(topic string, sb interface{}, opts ...SubscriberOption) Subscriber { - return newSubscriber(topic, sb, opts...) + return s.router.NewSubscriber(topic, sb, opts...) } func (s *rpcServer) Subscribe(sb Subscriber) error { - sub, ok := sb.(*subscriber) - if !ok { - return fmt.Errorf("invalid subscriber: expected *subscriber") - } - if len(sub.handlers) == 0 { - return fmt.Errorf("invalid subscriber: no handler functions") - } + s.Lock() + defer s.Unlock() - if err := validateSubscriber(sb); err != nil { + if err := s.router.Subscribe(sb); err != nil { return err } - s.Lock() - defer s.Unlock() - _, ok = s.subscribers[sub] - if ok { - return fmt.Errorf("subscriber %v already exists", s) - } - s.subscribers[sub] = nil + s.subscribers[sb] = nil return nil } @@ -469,6 +549,7 @@ func (s *rpcServer) Register() error { node.Metadata["protocol"] = "mucp" s.RLock() + // Maps are ordered randomly, sort the keys for consistency var handlerList []string for n, e := range s.handlers { @@ -477,27 +558,30 @@ func (s *rpcServer) Register() error { handlerList = append(handlerList, n) } } + sort.Strings(handlerList) - var subscriberList []*subscriber + var subscriberList []Subscriber for e := range s.subscribers { // Only advertise non internal subscribers if !e.Options().Internal { subscriberList = append(subscriberList, e) } } + sort.Slice(subscriberList, func(i, j int) bool { - return subscriberList[i].topic > subscriberList[j].topic + return subscriberList[i].Topic() > subscriberList[j].Topic() }) - var endpoints []*registry.Endpoint + endpoints := make([]*registry.Endpoint, 0, len(handlerList)+len(subscriberList)) + for _, n := range handlerList { endpoints = append(endpoints, s.handlers[n].Endpoints()...) } + for _, e := range subscriberList { endpoints = append(endpoints, e.Endpoints()...) } - s.RUnlock() service := ®istry.Service{ Name: config.Name, @@ -506,9 +590,10 @@ func (s *rpcServer) Register() error { Endpoints: endpoints, } - s.Lock() + // get registered value registered := s.registered - s.Unlock() + + s.RUnlock() if !registered { log.Logf("Registry [%s] Registering node: %s", config.Registry.String(), node.Id) @@ -530,9 +615,20 @@ func (s *rpcServer) Register() error { defer s.Unlock() s.registered = true + // set what we're advertising + s.opts.Advertise = addr - for sb, _ := range s.subscribers { - handler := s.createSubHandler(sb, s.opts) + // subscribe to the topic with own name + sub, err := s.opts.Broker.Subscribe(config.Name, s.HandleEvent) + if err != nil { + return err + } + + // save the subscriber + s.subscriber = sub + + // subscribe for all of the subscribers + for sb := range s.subscribers { var opts []broker.SubscribeOption if queue := sb.Options().Queue; len(queue) > 0 { opts = append(opts, broker.Queue(queue)) @@ -546,10 +642,11 @@ func (s *rpcServer) Register() error { opts = append(opts, broker.DisableAutoAck()) } - sub, err := config.Broker.Subscribe(sb.Topic(), handler, opts...) + sub, err := config.Broker.Subscribe(sb.Topic(), s.HandleEvent, opts...) if err != nil { return err } + log.Logf("Subscribing %s to topic: %s", node.Id, sub.Topic()) s.subscribers[sb] = []broker.Subscriber{sub} } @@ -617,6 +714,12 @@ func (s *rpcServer) Deregister() error { s.registered = false + // close the subscriber + if s.subscriber != nil { + s.subscriber.Unsubscribe() + s.subscriber = nil + } + for sb, subs := range s.subscribers { for _, sub := range subs { log.Logf("Unsubscribing %s from topic: %s", node.Id, sub.Topic()) @@ -744,9 +847,13 @@ func (s *rpcServer) Start() error { log.Logf("Server %s-%s deregister error: %s", config.Name, config.Id, err) } + s.Lock() + swg := s.wg + s.Unlock() + // wait for requests to finish - if s.wg != nil { - s.wg.Wait() + if swg != nil { + swg.Wait() } // close transport listener @@ -780,13 +887,10 @@ func (s *rpcServer) Stop() error { ch := make(chan error) s.exit <- ch - var err error - select { - case err = <-ch: - s.Lock() - s.started = false - s.Unlock() - } + err := <-ch + s.Lock() + s.started = false + s.Unlock() return err } diff --git a/server/rpc_util.go b/server/rpc_util.go new file mode 100644 index 00000000..a15be0ac --- /dev/null +++ b/server/rpc_util.go @@ -0,0 +1,32 @@ +package server + +import ( + "sync" +) + +// waitgroup for global management of connections +type waitGroup struct { + // local waitgroup + lg sync.WaitGroup + // global waitgroup + gg *sync.WaitGroup +} + +func (w *waitGroup) Add(i int) { + w.lg.Add(i) + if w.gg != nil { + w.gg.Add(i) + } +} + +func (w *waitGroup) Done() { + w.lg.Done() + if w.gg != nil { + w.gg.Done() + } +} + +func (w *waitGroup) Wait() { + // only wait on local group + w.lg.Wait() +} diff --git a/server/server.go b/server/server.go index 98f2cddb..0fcc3436 100644 --- a/server/server.go +++ b/server/server.go @@ -29,15 +29,26 @@ type Server interface { // Router handle serving messages type Router interface { + // ProcessMessage processes a message + ProcessMessage(context.Context, Message) error // ServeRequest processes a request to completion ServeRequest(context.Context, Request, Response) error } // Message is an async message interface type Message interface { + // Topic of the message Topic() string + // The decoded payload value Payload() interface{} + // The content type of the payload ContentType() string + // The raw headers of the message + Header() map[string]string + // The raw body of the message + Body() []byte + // Codec used to decode the message + Codec() codec.Reader } // Request is a synchronous request interface @@ -190,7 +201,7 @@ func Run() error { } ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) log.Logf("Received signal %s", <-ch) return Stop() diff --git a/server/subscriber.go b/server/subscriber.go index e3f29286..62de0ed8 100644 --- a/server/subscriber.go +++ b/server/subscriber.go @@ -1,17 +1,10 @@ package server import ( - "bytes" - "context" "fmt" "reflect" - "strings" - "github.com/micro/go-micro/broker" - "github.com/micro/go-micro/codec" - "github.com/micro/go-micro/metadata" "github.com/micro/go-micro/registry" - "github.com/micro/go-micro/util/buf" ) const ( @@ -165,119 +158,6 @@ func validateSubscriber(sub Subscriber) error { return nil } -func (s *rpcServer) createSubHandler(sb *subscriber, opts Options) broker.Handler { - return func(p broker.Event) 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 - } - - // create context - ctx := metadata.NewContext(context.Background(), hdr) - - results := make(chan error, len(sb.handlers)) - - for i := 0; i < len(sb.handlers); i++ { - handler := sb.handlers[i] - - var isVal bool - var req reflect.Value - - if handler.reqType.Kind() == reflect.Ptr { - req = reflect.New(handler.reqType.Elem()) - } else { - req = reflect.New(handler.reqType) - isVal = true - } - if isVal { - req = req.Elem() - } - - b := buf.New(bytes.NewBuffer(msg.Body)) - co := cf(b) - defer co.Close() - - if err := co.ReadHeader(&codec.Message{}, codec.Event); err != nil { - return err - } - - if err := co.ReadBody(req.Interface()); err != nil { - return err - } - - fn := func(ctx context.Context, msg Message) error { - var vals []reflect.Value - if sb.typ.Kind() != reflect.Func { - vals = append(vals, sb.rcvr) - } - if handler.ctxType != nil { - vals = append(vals, reflect.ValueOf(ctx)) - } - - vals = append(vals, reflect.ValueOf(msg.Payload())) - - returnValues := handler.method.Call(vals) - if err := returnValues[0].Interface(); err != nil { - return err.(error) - } - return nil - } - - for i := len(opts.SubWrappers); i > 0; i-- { - fn = opts.SubWrappers[i-1](fn) - } - - if s.wg != nil { - s.wg.Add(1) - } - - go func() { - if s.wg != nil { - defer s.wg.Done() - } - - results <- fn(ctx, &rpcMessage{ - topic: sb.topic, - contentType: ct, - payload: req.Interface(), - }) - }() - } - - var errors []string - - for i := 0; i < len(sb.handlers); i++ { - if err := <-results; err != nil { - errors = append(errors, err.Error()) - } - } - - if len(errors) > 0 { - return fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n")) - } - - return nil - } -} - func (s *subscriber) Topic() string { return s.topic } diff --git a/service.go b/service.go index eed7f8ac..0245bfd6 100644 --- a/service.go +++ b/service.go @@ -3,17 +3,21 @@ package micro import ( "os" "os/signal" + "runtime" "strings" "sync" "syscall" "github.com/micro/go-micro/client" "github.com/micro/go-micro/config/cmd" - "github.com/micro/go-micro/debug/handler" - "github.com/micro/go-micro/metadata" + "github.com/micro/go-micro/debug/profile" + "github.com/micro/go-micro/debug/profile/http" + "github.com/micro/go-micro/debug/profile/pprof" + "github.com/micro/go-micro/debug/service/handler" "github.com/micro/go-micro/plugin" "github.com/micro/go-micro/server" "github.com/micro/go-micro/util/log" + "github.com/micro/go-micro/util/wrapper" ) type service struct { @@ -25,18 +29,21 @@ type service struct { func newService(opts ...Option) Service { options := newOptions(opts...) - options.Client = &clientWrapper{ - options.Client, - metadata.Metadata{ - HeaderPrefix + "From-Service": options.Server.Options().Name, - }, - } + // service name + serviceName := options.Server.Options().Name + + // wrap client to inject From-Service header on any calls + options.Client = wrapper.FromService(serviceName, options.Client) return &service{ opts: options, } } +func (s *service) Name() string { + return s.opts.Server.Options().Name +} + // Init initialises options. Additionally it calls cmd.Init // which parses command line flags. cmd.Init is only called // on first Init. @@ -143,12 +150,42 @@ func (s *service) Run() error { ), ) + // start the profiler + // TODO: set as an option to the service, don't just use pprof + if prof := os.Getenv("MICRO_DEBUG_PROFILE"); len(prof) > 0 { + var profiler profile.Profile + + // to view mutex contention + runtime.SetMutexProfileFraction(5) + // to view blocking profile + runtime.SetBlockProfileRate(1) + + switch prof { + case "http": + profiler = http.NewProfile() + default: + service := s.opts.Server.Options().Name + version := s.opts.Server.Options().Version + id := s.opts.Server.Options().Id + profiler = pprof.NewProfile( + profile.Name(service + "." + version + "." + id), + ) + } + + if err := profiler.Start(); err != nil { + return err + } + defer profiler.Stop() + } + if err := s.Start(); err != nil { return err } ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) + if s.opts.Signal { + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) + } select { // wait on kill signal diff --git a/service/grpc/grpc_test.go b/service/grpc/grpc_test.go index bf7c0a95..5978c4fb 100644 --- a/service/grpc/grpc_test.go +++ b/service/grpc/grpc_test.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "sync" "testing" + "time" "github.com/micro/go-micro" "github.com/micro/go-micro/registry/memory" @@ -44,10 +45,10 @@ func TestGRPCService(t *testing.T) { hello.RegisterTestHandler(service.Server(), &testHandler{}) // run service + errCh := make(chan error, 1) go func() { - if err := service.Run(); err != nil { - t.Fatal(err) - } + defer close(errCh) + errCh <- service.Run() }() // wait for start @@ -57,13 +58,23 @@ func TestGRPCService(t *testing.T) { test := hello.NewTestService("test.service", service.Client()) // call service - rsp, err := test.Call(context.Background(), &hello.Request{ + ctx2, cancel2 := context.WithTimeout(context.Background(), time.Duration(time.Second)) + defer cancel2() + rsp, err := test.Call(ctx2, &hello.Request{ Name: "John", }) if err != nil { t.Fatal(err) } + // check server + select { + case err := <-errCh: + t.Fatal(err) + case <-time.After(time.Second): + break + } + // check message if rsp.Msg != "Hello John" { t.Fatalf("unexpected response %s", rsp.Msg) @@ -151,10 +162,10 @@ func TestGRPCTLSService(t *testing.T) { hello.RegisterTestHandler(service.Server(), &testHandler{}) // run service + errCh := make(chan error, 1) go func() { - if err := service.Run(); err != nil { - t.Fatal(err) - } + defer close(errCh) + errCh <- service.Run() }() // wait for start @@ -164,13 +175,23 @@ func TestGRPCTLSService(t *testing.T) { test := hello.NewTestService("test.service", service.Client()) // call service - rsp, err := test.Call(context.Background(), &hello.Request{ + ctx2, cancel2 := context.WithTimeout(context.Background(), time.Duration(time.Second)) + defer cancel2() + rsp, err := test.Call(ctx2, &hello.Request{ Name: "John", }) if err != nil { t.Fatal(err) } + // check server + select { + case err := <-errCh: + t.Fatal(err) + case <-time.After(time.Second): + break + } + // check message if rsp.Msg != "Hello John" { t.Fatalf("unexpected response %s", rsp.Msg) diff --git a/service/service.go b/service/service.go index 5b9d3027..aca8acd6 100644 --- a/service/service.go +++ b/service/service.go @@ -6,11 +6,20 @@ import ( "github.com/micro/go-micro/server" ) +// Service is an interface for a micro service type Service interface { + // The service name + Name() string + // Init initialises options Init(...Option) + // Options returns the current options Options() Options + // Client is used to call services Client() client.Client + // Server is for handling requests and events Server() server.Server + // Run the service Run() error + // The service implementation String() string } diff --git a/service_test.go b/service_test.go index 82195aed..79b4872c 100644 --- a/service_test.go +++ b/service_test.go @@ -8,9 +8,10 @@ import ( glog "github.com/go-log/log" "github.com/micro/go-micro/client" - proto "github.com/micro/go-micro/debug/proto" + proto "github.com/micro/go-micro/debug/service/proto" "github.com/micro/go-micro/registry/memory" "github.com/micro/go-micro/util/log" + "github.com/micro/go-micro/util/test" ) func testShutdown(wg *sync.WaitGroup, cancel func()) { @@ -29,8 +30,7 @@ func testService(ctx context.Context, wg *sync.WaitGroup, name string) Service { // add self wg.Add(1) - r := memory.NewRegistry() - r.(*memory.Registry).Services = testData + r := memory.NewRegistry(memory.Services(test.Data)) // create service return NewService( diff --git a/store/cloudflare/cloudflare.go b/store/cloudflare/cloudflare.go new file mode 100644 index 00000000..3b9a5e8c --- /dev/null +++ b/store/cloudflare/cloudflare.go @@ -0,0 +1,338 @@ +// Package cloudflare is a store implementation backed by cloudflare workers kv +// Note that the cloudflare workers KV API is eventually consistent. +package cloudflare + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "math" + "net/http" + "net/url" + "os" + "strconv" + "time" + + "github.com/micro/go-micro/config/options" + "github.com/micro/go-micro/store" + "github.com/pkg/errors" +) + +const ( + apiBaseURL = "https://api.cloudflare.com/client/v4/" +) + +type workersKV struct { + options.Options + // cf account id + account string + // cf api token + token string + // cf kv namespace + namespace string + // http client to use + httpClient *http.Client +} + +// apiResponse is a cloudflare v4 api response +type apiResponse struct { + Result []struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Expiration string `json:"expiration"` + Content string `json:"content"` + Proxiable bool `json:"proxiable"` + Proxied bool `json:"proxied"` + TTL int `json:"ttl"` + Priority int `json:"priority"` + Locked bool `json:"locked"` + ZoneID string `json:"zone_id"` + ZoneName string `json:"zone_name"` + ModifiedOn time.Time `json:"modified_on"` + CreatedOn time.Time `json:"created_on"` + } `json:"result"` + Success bool `json:"success"` + Errors []apiMessage `json:"errors"` + // not sure Messages is ever populated? + Messages []apiMessage `json:"messages"` + ResultInfo struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Count int `json:"count"` + TotalCount int `json:"total_count"` + } `json:"result_info"` +} + +// apiMessage is a Cloudflare v4 API Error +type apiMessage struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// getOptions returns account id, token and namespace +func getOptions() (string, string, string) { + accountID := os.Getenv("CF_ACCOUNT_ID") + apiToken := os.Getenv("CF_API_TOKEN") + namespace := os.Getenv("KV_NAMESPACE_ID") + + return accountID, apiToken, namespace +} + +func validateOptions(account, token, namespace string) { + if len(account) == 0 { + log.Fatal("Store: CF_ACCOUNT_ID is blank") + } + + if len(token) == 0 { + log.Fatal("Store: CF_API_TOKEN is blank") + } + + if len(namespace) == 0 { + log.Fatal("Store: KV_NAMESPACE_ID is blank") + } +} + +// In the cloudflare workers KV implemention, List() doesn't guarantee +// anything as the workers API is eventually consistent. +func (w *workersKV) List() ([]*store.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + path := fmt.Sprintf("accounts/%s/storage/kv/namespaces/%s/keys", w.account, w.namespace) + + response, _, _, err := w.request(ctx, http.MethodGet, path, nil, make(http.Header)) + if err != nil { + return nil, err + } + + a := &apiResponse{} + if err := json.Unmarshal(response, a); err != nil { + return nil, err + } + + if !a.Success { + messages := "" + for _, m := range a.Errors { + messages += strconv.Itoa(m.Code) + " " + m.Message + "\n" + } + return nil, errors.New(messages) + } + + keys := make([]string, 0, len(a.Result)) + + for _, r := range a.Result { + keys = append(keys, r.Name) + } + + return w.Read(keys...) +} + +func (w *workersKV) Read(keys ...string) ([]*store.Record, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + //nolint:prealloc + var records []*store.Record + + for _, k := range keys { + path := fmt.Sprintf("accounts/%s/storage/kv/namespaces/%s/values/%s", w.account, w.namespace, url.PathEscape(k)) + response, headers, status, err := w.request(ctx, http.MethodGet, path, nil, make(http.Header)) + if err != nil { + return records, err + } + if status < 200 || status >= 300 { + return records, errors.New("Received unexpected Status " + strconv.Itoa(status) + string(response)) + } + record := &store.Record{ + Key: k, + Value: response, + } + if expiry := headers.Get("Expiration"); len(expiry) != 0 { + expiryUnix, err := strconv.ParseInt(expiry, 10, 64) + if err != nil { + return records, err + } + record.Expiry = time.Until(time.Unix(expiryUnix, 0)) + } + records = append(records, record) + } + + return records, nil +} + +func (w *workersKV) Write(records ...*store.Record) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for _, r := range records { + path := fmt.Sprintf("accounts/%s/storage/kv/namespaces/%s/values/%s", w.account, w.namespace, url.PathEscape(r.Key)) + if r.Expiry != 0 { + // Minimum cloudflare TTL is 60 Seconds + exp := int(math.Max(60, math.Round(r.Expiry.Seconds()))) + path = path + "?expiration_ttl=" + strconv.Itoa(exp) + } + + headers := make(http.Header) + + resp, _, _, err := w.request(ctx, http.MethodPut, path, r.Value, headers) + if err != nil { + return err + } + + a := &apiResponse{} + if err := json.Unmarshal(resp, a); err != nil { + return err + } + + if !a.Success { + messages := "" + for _, m := range a.Errors { + messages += strconv.Itoa(m.Code) + " " + m.Message + "\n" + } + return errors.New(messages) + } + } + + return nil +} + +func (w *workersKV) Delete(keys ...string) error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for _, k := range keys { + path := fmt.Sprintf("accounts/%s/storage/kv/namespaces/%s/values/%s", w.account, w.namespace, url.PathEscape(k)) + resp, _, _, err := w.request(ctx, http.MethodDelete, path, nil, make(http.Header)) + if err != nil { + return err + } + + a := &apiResponse{} + if err := json.Unmarshal(resp, a); err != nil { + return err + } + + if !a.Success { + messages := "" + for _, m := range a.Errors { + messages += strconv.Itoa(m.Code) + " " + m.Message + "\n" + } + return errors.New(messages) + } + } + + return nil +} + +func (w *workersKV) request(ctx context.Context, method, path string, body interface{}, headers http.Header) ([]byte, http.Header, int, error) { + var jsonBody []byte + var err error + + if body != nil { + if paramBytes, ok := body.([]byte); ok { + jsonBody = paramBytes + } else { + jsonBody, err = json.Marshal(body) + if err != nil { + return nil, nil, 0, errors.Wrap(err, "error marshalling params to JSON") + } + } + } else { + jsonBody = nil + } + + var reqBody io.Reader + + if jsonBody != nil { + reqBody = bytes.NewReader(jsonBody) + } + + req, err := http.NewRequestWithContext(ctx, method, apiBaseURL+path, reqBody) + if err != nil { + return nil, nil, 0, errors.Wrap(err, "error creating new request") + } + + for key, value := range headers { + req.Header[key] = value + } + + // set token if it exists + if len(w.token) > 0 { + req.Header.Set("Authorization", "Bearer "+w.token) + } + + // set the user agent to micro + req.Header.Set("User-Agent", "micro/1.0 (https://micro.mu)") + + // Official cloudflare client does exponential backoff here + // TODO: retry and use util/backoff + resp, err := w.httpClient.Do(req) + if err != nil { + return nil, nil, 0, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return respBody, resp.Header, resp.StatusCode, err + } + + return respBody, resp.Header, resp.StatusCode, nil +} + +// New returns a cloudflare Store implementation. +// Account ID, Token and Namespace must either be passed as options or +// environment variables. If set as env vars we expect the following; +// CF_API_TOKEN to a cloudflare API token scoped to Workers KV. +// CF_ACCOUNT_ID to contain a string with your cloudflare account ID. +// KV_NAMESPACE_ID to contain the namespace UUID for your KV storage. +func NewStore(opts ...options.Option) store.Store { + // create new Options + options := options.NewOptions(opts...) + + // get values from the environment + account, token, namespace := getOptions() + + // set api token from options if exists + if apiToken, ok := options.Values().Get("CF_API_TOKEN"); ok { + tk, ok := apiToken.(string) + if !ok { + log.Fatal("Store: Option CF_API_TOKEN contains a non-string") + } + token = tk + } + + // set account id from options if exists + if accountID, ok := options.Values().Get("CF_ACCOUNT_ID"); ok { + id, ok := accountID.(string) + if !ok { + log.Fatal("Store: Option CF_ACCOUNT_ID contains a non-string") + } + account = id + } + + // set namespace from options if exists + if uuid, ok := options.Values().Get("KV_NAMESPACE_ID"); ok { + ns, ok := uuid.(string) + if !ok { + log.Fatal("Store: Option KV_NAMESPACE_ID contains a non-string") + } + namespace = ns + } + + // validate options are not blank or log.Fatal + validateOptions(account, token, namespace) + + return &workersKV{ + account: account, + namespace: namespace, + token: token, + Options: options, + httpClient: &http.Client{}, + } +} diff --git a/store/cloudflare/cloudflare_test.go b/store/cloudflare/cloudflare_test.go new file mode 100644 index 00000000..525eeecd --- /dev/null +++ b/store/cloudflare/cloudflare_test.go @@ -0,0 +1,87 @@ +package cloudflare + +import ( + "math/rand" + "os" + "strconv" + "testing" + "time" + + "github.com/micro/go-micro/store" +) + +func TestCloudflare(t *testing.T) { + apiToken, accountID := os.Getenv("CF_API_TOKEN"), os.Getenv("CF_ACCOUNT_ID") + kvID := os.Getenv("KV_NAMESPACE_ID") + if len(apiToken) == 0 || len(accountID) == 0 || len(kvID) == 0 { + t.Skip("No Cloudflare API keys available, skipping test") + } + rand.Seed(time.Now().UnixNano()) + randomK := strconv.Itoa(rand.Int()) + randomV := strconv.Itoa(rand.Int()) + + wkv := NewStore( + Token(apiToken), + Account(accountID), + Namespace(kvID), + ) + + records, err := wkv.List() + if err != nil { + t.Fatalf("List: %s\n", err.Error()) + } else { + t.Log("Listed " + strconv.Itoa(len(records)) + " records") + } + + err = wkv.Write( + &store.Record{ + Key: randomK, + Value: []byte(randomV), + }, + &store.Record{ + Key: "expirationtest", + Value: []byte("This message will self destruct"), + Expiry: 75 * time.Second, + }, + ) + if err != nil { + t.Errorf("Write: %s", err.Error()) + } + + // This might be needed for cloudflare eventual consistency + time.Sleep(1 * time.Minute) + + r, err := wkv.Read(randomK) + if err != nil { + t.Errorf("Read: %s\n", err.Error()) + } + if len(r) != 1 { + t.Errorf("Expected to read 1 key, got %d keys\n", len(r)) + } + if string(r[0].Value) != randomV { + t.Errorf("Read: expected %s, got %s\n", randomK, string(r[0].Value)) + } + + r, err = wkv.Read("expirationtest") + if err != nil { + t.Errorf("Read: expirationtest should still exist") + } + if r[0].Expiry == 0 { + t.Error("Expected r to have an expiry") + } else { + t.Log(r[0].Expiry) + } + + time.Sleep(20 * time.Second) + r, err = wkv.Read("expirationtest") + if err == nil && len(r) != 0 { + t.Error("Read: Managed to read expirationtest, but it should have expired") + t.Log(err, r[0].Key, string(r[0].Value), r[0].Expiry, len(r)) + } + + err = wkv.Delete(randomK) + if err != nil { + t.Errorf("Delete: %s\n", err.Error()) + } + +} diff --git a/store/cloudflare/options.go b/store/cloudflare/options.go new file mode 100644 index 00000000..36dc7c10 --- /dev/null +++ b/store/cloudflare/options.go @@ -0,0 +1,23 @@ +package cloudflare + +import ( + "github.com/micro/go-micro/config/options" +) + +// Token sets the cloudflare api token +func Token(t string) options.Option { + // TODO: change to store.cf.api_token + return options.WithValue("CF_API_TOKEN", t) +} + +// Account sets the cloudflare account id +func Account(id string) options.Option { + // TODO: change to store.cf.account_id + return options.WithValue("CF_ACCOUNT_ID", id) +} + +// Namespace sets the KV namespace +func Namespace(ns string) options.Option { + // TODO: change to store.cf.namespace + return options.WithValue("KV_NAMESPACE_ID", ns) +} diff --git a/store/etcd/etcd.go b/store/etcd/etcd.go new file mode 100644 index 00000000..8ffa2d7d --- /dev/null +++ b/store/etcd/etcd.go @@ -0,0 +1,119 @@ +// Package etcd is an etcd v3 implementation of kv +package etcd + +import ( + "context" + "log" + + client "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/mvcc/mvccpb" + "github.com/micro/go-micro/config/options" + "github.com/micro/go-micro/store" +) + +type ekv struct { + options.Options + kv client.KV +} + +func (e *ekv) Read(keys ...string) ([]*store.Record, error) { + //nolint:prealloc + var values []*mvccpb.KeyValue + + for _, key := range keys { + keyval, err := e.kv.Get(context.Background(), key) + if err != nil { + return nil, err + } + + if keyval == nil || len(keyval.Kvs) == 0 { + return nil, store.ErrNotFound + } + + values = append(values, keyval.Kvs...) + } + + records := make([]*store.Record, 0, len(values)) + + for _, kv := range values { + records = append(records, &store.Record{ + Key: string(kv.Key), + Value: kv.Value, + // TODO: implement expiry + }) + } + + return records, nil +} + +func (e *ekv) Delete(keys ...string) error { + var gerr error + for _, key := range keys { + _, err := e.kv.Delete(context.Background(), key) + if err != nil { + gerr = err + } + } + return gerr +} + +func (e *ekv) Write(records ...*store.Record) error { + var gerr error + for _, record := range records { + // TODO create lease to expire keys + _, err := e.kv.Put(context.Background(), record.Key, string(record.Value)) + if err != nil { + gerr = err + } + } + return gerr +} + +func (e *ekv) List() ([]*store.Record, error) { + keyval, err := e.kv.Get(context.Background(), "/", client.WithPrefix()) + if err != nil { + return nil, err + } + if keyval == nil || len(keyval.Kvs) == 0 { + return nil, nil + } + vals := make([]*store.Record, 0, len(keyval.Kvs)) + for _, keyv := range keyval.Kvs { + vals = append(vals, &store.Record{ + Key: string(keyv.Key), + Value: keyv.Value, + }) + } + return vals, nil +} + +func (e *ekv) String() string { + return "etcd" +} + +func NewStore(opts ...options.Option) store.Store { + options := options.NewOptions(opts...) + + var endpoints []string + + if e, ok := options.Values().Get("store.nodes"); ok { + endpoints = e.([]string) + } + + if len(endpoints) == 0 { + endpoints = []string{"http://127.0.0.1:2379"} + } + + // TODO: parse addresses + c, err := client.New(client.Config{ + Endpoints: endpoints, + }) + if err != nil { + log.Fatal(err) + } + + return &ekv{ + Options: options, + kv: client.NewKV(c), + } +} diff --git a/data/store/memory/memory.go b/store/memory/memory.go similarity index 54% rename from data/store/memory/memory.go rename to store/memory/memory.go index bb7892e8..01d1a44c 100644 --- a/data/store/memory/memory.go +++ b/store/memory/memory.go @@ -6,7 +6,7 @@ import ( "time" "github.com/micro/go-micro/config/options" - "github.com/micro/go-micro/data/store" + "github.com/micro/go-micro/store" ) type memoryStore struct { @@ -21,10 +21,11 @@ type memoryRecord struct { c time.Time } -func (m *memoryStore) Dump() ([]*store.Record, error) { +func (m *memoryStore) List() ([]*store.Record, error) { m.RLock() defer m.RUnlock() + //nolint:prealloc var values []*store.Record for _, v := range m.values { @@ -48,51 +49,62 @@ func (m *memoryStore) Dump() ([]*store.Record, error) { return values, nil } -func (m *memoryStore) Read(key string) (*store.Record, error) { +func (m *memoryStore) Read(keys ...string) ([]*store.Record, error) { m.RLock() defer m.RUnlock() - v, ok := m.values[key] - if !ok { - return nil, store.ErrNotFound - } + //nolint:prealloc + var records []*store.Record - // get expiry - d := v.r.Expiry - t := time.Since(v.c) - - // expired - if d > time.Duration(0) { - if t > d { + for _, key := range keys { + v, ok := m.values[key] + if !ok { return nil, store.ErrNotFound } - // update expiry - v.r.Expiry -= t - v.c = time.Now() + + // get expiry + d := v.r.Expiry + t := time.Since(v.c) + + // expired + if d > time.Duration(0) { + if t > d { + return nil, store.ErrNotFound + } + // update expiry + v.r.Expiry -= t + v.c = time.Now() + } + + records = append(records, v.r) } - return v.r, nil + return records, nil } -func (m *memoryStore) Write(r *store.Record) error { +func (m *memoryStore) Write(records ...*store.Record) error { m.Lock() defer m.Unlock() - // set the record - m.values[r.Key] = &memoryRecord{ - r: r, - c: time.Now(), + for _, r := range records { + // set the record + m.values[r.Key] = &memoryRecord{ + r: r, + c: time.Now(), + } } return nil } -func (m *memoryStore) Delete(key string) error { +func (m *memoryStore) Delete(keys ...string) error { m.Lock() defer m.Unlock() - // delete the value - delete(m.values, key) + for _, key := range keys { + // delete the value + delete(m.values, key) + } return nil } diff --git a/data/store/memory/memory_test.go b/store/memory/memory_test.go similarity index 88% rename from data/store/memory/memory_test.go rename to store/memory/memory_test.go index 44483869..de889856 100644 --- a/data/store/memory/memory_test.go +++ b/store/memory/memory_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/micro/go-micro/data/store" + "github.com/micro/go-micro/store" ) func TestReadRecordExpire(t *testing.T) { @@ -25,7 +25,7 @@ func TestReadRecordExpire(t *testing.T) { if err != nil { t.Fatal(err) } - if rrec.Expiry >= expire { + if rrec[0].Expiry >= expire { t.Fatal("expiry of read record is not changed") } diff --git a/store/mock/store.go b/store/mock/store.go new file mode 100644 index 00000000..fe296b02 --- /dev/null +++ b/store/mock/store.go @@ -0,0 +1,103 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mock + +import mock "github.com/stretchr/testify/mock" +import store "github.com/micro/go-micro/store" + +// Store is an autogenerated mock type for the Store type +type Store struct { + mock.Mock +} + +// Delete provides a mock function with given fields: key +func (_m *Store) Delete(key ...string) error { + _va := make([]interface{}, len(key)) + for _i := range key { + _va[_i] = key[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...string) error); ok { + r0 = rf(key...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// List provides a mock function with given fields: +func (_m *Store) List() ([]*store.Record, error) { + ret := _m.Called() + + var r0 []*store.Record + if rf, ok := ret.Get(0).(func() []*store.Record); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*store.Record) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Read provides a mock function with given fields: key +func (_m *Store) Read(key ...string) ([]*store.Record, error) { + _va := make([]interface{}, len(key)) + for _i := range key { + _va[_i] = key[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 []*store.Record + if rf, ok := ret.Get(0).(func(...string) []*store.Record); ok { + r0 = rf(key...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*store.Record) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(...string) error); ok { + r1 = rf(key...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Write provides a mock function with given fields: rec +func (_m *Store) Write(rec ...*store.Record) error { + _va := make([]interface{}, len(rec)) + for _i := range rec { + _va[_i] = rec[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...*store.Record) error); ok { + r0 = rf(rec...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/data/store/options.go b/store/options.go similarity index 54% rename from data/store/options.go rename to store/options.go index cf780a58..99014362 100644 --- a/data/store/options.go +++ b/store/options.go @@ -4,7 +4,7 @@ import ( "github.com/micro/go-micro/config/options" ) -// Set the nodes used to back the store +// Nodes is a list of nodes used to back the store func Nodes(a ...string) options.Option { return options.WithValue("store.nodes", a) } @@ -13,3 +13,9 @@ func Nodes(a ...string) options.Option { func Prefix(p string) options.Option { return options.WithValue("store.prefix", p) } + +// Namespace offers a way to have multiple isolated +// stores in the same backend, if supported. +func Namespace(n string) options.Option { + return options.WithValue("store.namespace", n) +} diff --git a/store/postgresql/postgresql.go b/store/postgresql/postgresql.go new file mode 100644 index 00000000..58fa935c --- /dev/null +++ b/store/postgresql/postgresql.go @@ -0,0 +1,251 @@ +// Package postgresql implements a micro Store backed by sql +package postgresql + +import ( + "database/sql" + "fmt" + "strings" + "time" + "unicode" + + "github.com/lib/pq" + "github.com/pkg/errors" + + "github.com/micro/go-micro/config/options" + "github.com/micro/go-micro/store" +) + +// DefaultNamespace is the namespace that the sql store +// will use if no namespace is provided. +const DefaultNamespace = "micro" + +type sqlStore struct { + db *sql.DB + + table string + options.Options +} + +// List all the known records +func (s *sqlStore) List() ([]*store.Record, error) { + q, err := s.db.Prepare(fmt.Sprintf("SELECT key, value, expiry FROM micro.%s;", s.table)) + if err != nil { + return nil, err + } + var records []*store.Record + var timehelper pq.NullTime + rows, err := q.Query() + if err != nil { + if err == sql.ErrNoRows { + return records, nil + } + return nil, err + } + defer rows.Close() + for rows.Next() { + record := &store.Record{} + if err := rows.Scan(&record.Key, &record.Value, &timehelper); err != nil { + return records, err + } + if timehelper.Valid { + if timehelper.Time.Before(time.Now()) { + // record has expired + go s.Delete(record.Key) + } else { + record.Expiry = time.Until(timehelper.Time) + records = append(records, record) + } + } else { + records = append(records, record) + } + + } + rowErr := rows.Close() + if rowErr != nil { + // transaction rollback or something + return records, rowErr + } + if err := rows.Err(); err != nil { + return records, err + } + return records, nil +} + +// Read all records with keys +func (s *sqlStore) Read(keys ...string) ([]*store.Record, error) { + q, err := s.db.Prepare(fmt.Sprintf("SELECT key, value, expiry FROM micro.%s WHERE key = $1;", s.table)) + if err != nil { + return nil, err + } + var records []*store.Record + var timehelper pq.NullTime + for _, key := range keys { + row := q.QueryRow(key) + record := &store.Record{} + if err := row.Scan(&record.Key, &record.Value, &timehelper); err != nil { + if err == sql.ErrNoRows { + return records, store.ErrNotFound + } + return records, err + } + if timehelper.Valid { + if timehelper.Time.Before(time.Now()) { + // record has expired + go s.Delete(key) + return records, store.ErrNotFound + } + record.Expiry = time.Until(timehelper.Time) + records = append(records, record) + } else { + records = append(records, record) + } + } + return records, nil +} + +// Write records +func (s *sqlStore) Write(rec ...*store.Record) error { + q, err := s.db.Prepare(fmt.Sprintf(`INSERT INTO micro.%s(key, value, expiry) + VALUES ($1, $2::bytea, $3) + ON CONFLICT (key) + DO UPDATE + SET value = EXCLUDED.value, expiry = EXCLUDED.expiry;`, s.table)) + if err != nil { + return err + } + for _, r := range rec { + var err error + if r.Expiry != 0 { + _, err = q.Exec(r.Key, r.Value, time.Now().Add(r.Expiry)) + } else { + _, err = q.Exec(r.Key, r.Value, nil) + } + if err != nil { + return errors.Wrap(err, "Couldn't insert record "+r.Key) + } + } + + return nil +} + +// Delete records with keys +func (s *sqlStore) Delete(keys ...string) error { + q, err := s.db.Prepare(fmt.Sprintf("DELETE FROM micro.%s WHERE key = $1;", s.table)) + if err != nil { + return err + } + for _, key := range keys { + result, err := q.Exec(key) + if err != nil { + return err + } + _, err = result.RowsAffected() + if err != nil { + return err + } + } + return nil +} + +func (s *sqlStore) initDB(options options.Options) error { + // Get the store.namespace option, or use sql.DefaultNamespace + namespaceOpt, found := options.Values().Get("store.namespace") + if !found { + s.table = DefaultNamespace + } else { + if namespace, ok := namespaceOpt.(string); ok { + s.table = namespace + } else { + return errors.New("store.namespace option must be a string") + } + } + + // Create "micro" schema + schema, err := s.db.Prepare("CREATE SCHEMA IF NOT EXISTS micro ;") + if err != nil { + return err + } + _, err = schema.Exec() + if err != nil { + return errors.Wrap(err, "Couldn't create Schema") + } + + // Create a table for the Store namespace + tableq, err := s.db.Prepare(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS micro.%s + ( + key text COLLATE "default" NOT NULL, + value bytea, + expiry timestamp with time zone, + CONSTRAINT %s_pkey PRIMARY KEY (key) + );`, s.table, s.table)) + if err != nil { + return errors.Wrap(err, "SQL statement preparation failed") + } + _, err = tableq.Exec() + if err != nil { + return errors.Wrap(err, "Couldn't create table") + } + + return nil +} + +// New returns a new micro Store backed by sql +func New(opts ...options.Option) (store.Store, error) { + options := options.NewOptions(opts...) + driver, dataSourceName, err := validateOptions(options) + if err != nil { + return nil, err + } + if !strings.Contains(dataSourceName, " ") { + dataSourceName = fmt.Sprintf("host=%s", dataSourceName) + } + db, err := sql.Open(driver, dataSourceName) + if err != nil { + return nil, err + } + if err := db.Ping(); err != nil { + return nil, err + } + s := &sqlStore{ + db: db, + } + + return s, s.initDB(options) +} + +// validateOptions checks whether the provided options are valid, then returns the driver +// and data source name. +func validateOptions(options options.Options) (driver, dataSourceName string, err error) { + driverOpt, found := options.Values().Get("store.sql.driver") + if !found { + return "", "", errors.New("No store.sql.driver option specified") + } + nodesOpt, found := options.Values().Get("store.nodes") + if !found { + return "", "", errors.New("No store.nodes option specified (expected a database connection string)") + } + driver, ok := driverOpt.(string) + if !ok { + return "", "", errors.New("store.sql.driver option must be a string") + } + nodes, ok := nodesOpt.([]string) + if !ok { + return "", "", errors.New("store.nodes option must be a []string") + } + if len(nodes) != 1 { + return "", "", errors.New("expected only 1 store.nodes option") + } + namespaceOpt, found := options.Values().Get("store.namespace") + if found { + namespace, ok := namespaceOpt.(string) + if !ok { + return "", "", errors.New("store.namespace must me a string") + } + for _, r := range namespace { + if !unicode.IsLetter(r) { + return "", "", errors.New("store.namespace must only contain letters") + } + } + } + return driver, nodes[0], nil +} diff --git a/store/postgresql/postgresql_test.go b/store/postgresql/postgresql_test.go new file mode 100644 index 00000000..fffc974e --- /dev/null +++ b/store/postgresql/postgresql_test.go @@ -0,0 +1,95 @@ +package postgresql + +import ( + "database/sql" + "fmt" + "testing" + "time" + + "github.com/kr/pretty" + "github.com/micro/go-micro/store" +) + +func TestSQL(t *testing.T) { + connection := fmt.Sprintf( + "host=%s port=%d user=%s sslmode=disable dbname=%s", + "localhost", + 5432, + "jake", + "test", + ) + db, err := sql.Open("postgres", connection) + if err != nil { + t.Fatal(err) + } + if err := db.Ping(); err != nil { + t.Skip(err) + } + db.Close() + + sqlStore, err := New( + store.Namespace("testsql"), + store.Nodes(connection), + ) + if err != nil { + t.Fatal(err.Error()) + } + + records, err := sqlStore.List() + if err != nil { + t.Error(err) + } else { + t.Logf("%# v\n", pretty.Formatter(records)) + } + + err = sqlStore.Write( + &store.Record{ + Key: "test", + Value: []byte("foo"), + }, + &store.Record{ + Key: "bar", + Value: []byte("baz"), + }, + &store.Record{ + Key: "qux", + Value: []byte("aasad"), + }, + ) + if err != nil { + t.Error(err) + } + err = sqlStore.Delete("qux") + if err != nil { + t.Error(err) + } + + err = sqlStore.Write(&store.Record{ + Key: "test", + Value: []byte("bar"), + Expiry: time.Minute, + }) + if err != nil { + t.Error(err) + } + + records, err = sqlStore.Read("test") + if err != nil { + t.Error(err) + } + t.Logf("%# v\n", pretty.Formatter(records)) + if string(records[0].Value) != "bar" { + t.Error("Expected bar, got ", string(records[0].Value)) + } + + time.Sleep(61 * time.Second) + _, err = sqlStore.Read("test") + switch err { + case nil: + t.Error("Key test should have expired") + default: + t.Error(err) + case store.ErrNotFound: + break + } +} diff --git a/store/service/proto/store.pb.go b/store/service/proto/store.pb.go new file mode 100644 index 00000000..8aaa8a3f --- /dev/null +++ b/store/service/proto/store.pb.go @@ -0,0 +1,417 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: micro/go-micro/store/service/proto/store.proto + +package go_micro_store + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// 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 + +type Record struct { + // key of the record + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // value in the record + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + // timestamp in unix seconds + Expiry int64 `protobuf:"varint,3,opt,name=expiry,proto3" json:"expiry,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Record) Reset() { *m = Record{} } +func (m *Record) String() string { return proto.CompactTextString(m) } +func (*Record) ProtoMessage() {} +func (*Record) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{0} +} + +func (m *Record) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Record.Unmarshal(m, b) +} +func (m *Record) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Record.Marshal(b, m, deterministic) +} +func (m *Record) XXX_Merge(src proto.Message) { + xxx_messageInfo_Record.Merge(m, src) +} +func (m *Record) XXX_Size() int { + return xxx_messageInfo_Record.Size(m) +} +func (m *Record) XXX_DiscardUnknown() { + xxx_messageInfo_Record.DiscardUnknown(m) +} + +var xxx_messageInfo_Record proto.InternalMessageInfo + +func (m *Record) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +func (m *Record) GetValue() []byte { + if m != nil { + return m.Value + } + return nil +} + +func (m *Record) GetExpiry() int64 { + if m != nil { + return m.Expiry + } + return 0 +} + +type ReadRequest struct { + Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReadRequest) Reset() { *m = ReadRequest{} } +func (m *ReadRequest) String() string { return proto.CompactTextString(m) } +func (*ReadRequest) ProtoMessage() {} +func (*ReadRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{1} +} + +func (m *ReadRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReadRequest.Unmarshal(m, b) +} +func (m *ReadRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReadRequest.Marshal(b, m, deterministic) +} +func (m *ReadRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReadRequest.Merge(m, src) +} +func (m *ReadRequest) XXX_Size() int { + return xxx_messageInfo_ReadRequest.Size(m) +} +func (m *ReadRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ReadRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ReadRequest proto.InternalMessageInfo + +func (m *ReadRequest) GetKeys() []string { + if m != nil { + return m.Keys + } + return nil +} + +type ReadResponse struct { + Records []*Record `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReadResponse) Reset() { *m = ReadResponse{} } +func (m *ReadResponse) String() string { return proto.CompactTextString(m) } +func (*ReadResponse) ProtoMessage() {} +func (*ReadResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{2} +} + +func (m *ReadResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReadResponse.Unmarshal(m, b) +} +func (m *ReadResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReadResponse.Marshal(b, m, deterministic) +} +func (m *ReadResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReadResponse.Merge(m, src) +} +func (m *ReadResponse) XXX_Size() int { + return xxx_messageInfo_ReadResponse.Size(m) +} +func (m *ReadResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ReadResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ReadResponse proto.InternalMessageInfo + +func (m *ReadResponse) GetRecords() []*Record { + if m != nil { + return m.Records + } + return nil +} + +type WriteRequest struct { + Records []*Record `protobuf:"bytes,2,rep,name=records,proto3" json:"records,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *WriteRequest) Reset() { *m = WriteRequest{} } +func (m *WriteRequest) String() string { return proto.CompactTextString(m) } +func (*WriteRequest) ProtoMessage() {} +func (*WriteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{3} +} + +func (m *WriteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_WriteRequest.Unmarshal(m, b) +} +func (m *WriteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_WriteRequest.Marshal(b, m, deterministic) +} +func (m *WriteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_WriteRequest.Merge(m, src) +} +func (m *WriteRequest) XXX_Size() int { + return xxx_messageInfo_WriteRequest.Size(m) +} +func (m *WriteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_WriteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_WriteRequest proto.InternalMessageInfo + +func (m *WriteRequest) GetRecords() []*Record { + if m != nil { + return m.Records + } + return nil +} + +type WriteResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *WriteResponse) Reset() { *m = WriteResponse{} } +func (m *WriteResponse) String() string { return proto.CompactTextString(m) } +func (*WriteResponse) ProtoMessage() {} +func (*WriteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{4} +} + +func (m *WriteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_WriteResponse.Unmarshal(m, b) +} +func (m *WriteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_WriteResponse.Marshal(b, m, deterministic) +} +func (m *WriteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_WriteResponse.Merge(m, src) +} +func (m *WriteResponse) XXX_Size() int { + return xxx_messageInfo_WriteResponse.Size(m) +} +func (m *WriteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_WriteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_WriteResponse proto.InternalMessageInfo + +type DeleteRequest struct { + Keys []string `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DeleteRequest) Reset() { *m = DeleteRequest{} } +func (m *DeleteRequest) String() string { return proto.CompactTextString(m) } +func (*DeleteRequest) ProtoMessage() {} +func (*DeleteRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{5} +} + +func (m *DeleteRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DeleteRequest.Unmarshal(m, b) +} +func (m *DeleteRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DeleteRequest.Marshal(b, m, deterministic) +} +func (m *DeleteRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_DeleteRequest.Merge(m, src) +} +func (m *DeleteRequest) XXX_Size() int { + return xxx_messageInfo_DeleteRequest.Size(m) +} +func (m *DeleteRequest) XXX_DiscardUnknown() { + xxx_messageInfo_DeleteRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_DeleteRequest proto.InternalMessageInfo + +func (m *DeleteRequest) GetKeys() []string { + if m != nil { + return m.Keys + } + return nil +} + +type DeleteResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *DeleteResponse) Reset() { *m = DeleteResponse{} } +func (m *DeleteResponse) String() string { return proto.CompactTextString(m) } +func (*DeleteResponse) ProtoMessage() {} +func (*DeleteResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{6} +} + +func (m *DeleteResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_DeleteResponse.Unmarshal(m, b) +} +func (m *DeleteResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_DeleteResponse.Marshal(b, m, deterministic) +} +func (m *DeleteResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_DeleteResponse.Merge(m, src) +} +func (m *DeleteResponse) XXX_Size() int { + return xxx_messageInfo_DeleteResponse.Size(m) +} +func (m *DeleteResponse) XXX_DiscardUnknown() { + xxx_messageInfo_DeleteResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_DeleteResponse proto.InternalMessageInfo + +type ListRequest struct { + // optional key + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ListRequest) Reset() { *m = ListRequest{} } +func (m *ListRequest) String() string { return proto.CompactTextString(m) } +func (*ListRequest) ProtoMessage() {} +func (*ListRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{7} +} + +func (m *ListRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ListRequest.Unmarshal(m, b) +} +func (m *ListRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ListRequest.Marshal(b, m, deterministic) +} +func (m *ListRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ListRequest.Merge(m, src) +} +func (m *ListRequest) XXX_Size() int { + return xxx_messageInfo_ListRequest.Size(m) +} +func (m *ListRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ListRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ListRequest proto.InternalMessageInfo + +func (m *ListRequest) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +type ListResponse struct { + Records []*Record `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ListResponse) Reset() { *m = ListResponse{} } +func (m *ListResponse) String() string { return proto.CompactTextString(m) } +func (*ListResponse) ProtoMessage() {} +func (*ListResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f84ccc98e143ed3e, []int{8} +} + +func (m *ListResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ListResponse.Unmarshal(m, b) +} +func (m *ListResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ListResponse.Marshal(b, m, deterministic) +} +func (m *ListResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ListResponse.Merge(m, src) +} +func (m *ListResponse) XXX_Size() int { + return xxx_messageInfo_ListResponse.Size(m) +} +func (m *ListResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ListResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ListResponse proto.InternalMessageInfo + +func (m *ListResponse) GetRecords() []*Record { + if m != nil { + return m.Records + } + return nil +} + +func init() { + proto.RegisterType((*Record)(nil), "go.micro.store.Record") + proto.RegisterType((*ReadRequest)(nil), "go.micro.store.ReadRequest") + proto.RegisterType((*ReadResponse)(nil), "go.micro.store.ReadResponse") + proto.RegisterType((*WriteRequest)(nil), "go.micro.store.WriteRequest") + proto.RegisterType((*WriteResponse)(nil), "go.micro.store.WriteResponse") + proto.RegisterType((*DeleteRequest)(nil), "go.micro.store.DeleteRequest") + proto.RegisterType((*DeleteResponse)(nil), "go.micro.store.DeleteResponse") + proto.RegisterType((*ListRequest)(nil), "go.micro.store.ListRequest") + proto.RegisterType((*ListResponse)(nil), "go.micro.store.ListResponse") +} + +func init() { + proto.RegisterFile("micro/go-micro/store/service/proto/store.proto", fileDescriptor_f84ccc98e143ed3e) +} + +var fileDescriptor_f84ccc98e143ed3e = []byte{ + // 333 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0x4d, 0x4f, 0xc2, 0x40, + 0x10, 0x86, 0x59, 0x0a, 0x35, 0x0c, 0x1f, 0x92, 0x89, 0x21, 0x0d, 0x7e, 0xd5, 0x7a, 0xe9, 0xc5, + 0x42, 0xf0, 0x0f, 0x98, 0xf8, 0x11, 0x4d, 0x3c, 0xad, 0x07, 0xcf, 0x08, 0x13, 0xd2, 0x80, 0x2e, + 0xee, 0x16, 0x62, 0xff, 0x90, 0xbf, 0xd3, 0xec, 0x6e, 0xab, 0xc5, 0x42, 0x62, 0xbc, 0xcd, 0xee, + 0xbc, 0xf3, 0xec, 0xdb, 0x79, 0x0b, 0xd1, 0x6b, 0x3c, 0x91, 0x62, 0x30, 0x13, 0x17, 0xb6, 0x50, + 0x89, 0x90, 0x34, 0x50, 0x24, 0xd7, 0xf1, 0x84, 0x06, 0x4b, 0x29, 0x92, 0xec, 0x2e, 0x32, 0x35, + 0x76, 0x66, 0xc2, 0x8e, 0x44, 0xe6, 0x36, 0xb8, 0x07, 0x97, 0xd3, 0x44, 0xc8, 0x29, 0x76, 0xc1, + 0x99, 0x53, 0xea, 0x31, 0x9f, 0x85, 0x0d, 0xae, 0x4b, 0x3c, 0x80, 0xfa, 0x7a, 0xbc, 0x58, 0x91, + 0x57, 0xf5, 0x59, 0xd8, 0xe2, 0xf6, 0x80, 0x3d, 0x70, 0xe9, 0x63, 0x19, 0xcb, 0xd4, 0x73, 0x7c, + 0x16, 0x3a, 0x3c, 0x3b, 0x05, 0x67, 0xd0, 0xe4, 0x34, 0x9e, 0x72, 0x7a, 0x5f, 0x91, 0x4a, 0x10, + 0xa1, 0x36, 0xa7, 0x54, 0x79, 0xcc, 0x77, 0xc2, 0x06, 0x37, 0x75, 0x70, 0x05, 0x2d, 0x2b, 0x51, + 0x4b, 0xf1, 0xa6, 0x08, 0x87, 0xb0, 0x27, 0xcd, 0xe3, 0x56, 0xd6, 0x1c, 0xf5, 0xa2, 0x4d, 0x7b, + 0x91, 0xf5, 0xc6, 0x73, 0x99, 0x26, 0x3c, 0xcb, 0x38, 0xa1, 0xfc, 0x95, 0x02, 0xa1, 0xfa, 0x37, + 0xc2, 0x3e, 0xb4, 0x33, 0x82, 0x35, 0x11, 0x9c, 0x43, 0xfb, 0x86, 0x16, 0xf4, 0xc3, 0xdc, 0xe6, + 0xbc, 0x0b, 0x9d, 0x5c, 0x94, 0x8d, 0x9d, 0x42, 0xf3, 0x31, 0x56, 0x49, 0x3e, 0x54, 0xda, 0x9e, + 0xb6, 0x6a, 0x05, 0xff, 0xfd, 0xd8, 0xd1, 0x67, 0x15, 0xea, 0x4f, 0xba, 0x83, 0xb7, 0x50, 0xd3, + 0x2c, 0x3c, 0xfc, 0x3d, 0x52, 0xb0, 0xd0, 0x3f, 0xda, 0xde, 0xcc, 0xfc, 0x56, 0x86, 0x0c, 0xaf, + 0xa1, 0xa6, 0xf7, 0x5f, 0xc6, 0x14, 0x82, 0x2b, 0x63, 0x8a, 0x91, 0x05, 0x15, 0xbc, 0x83, 0xba, + 0x59, 0x20, 0x96, 0x84, 0xc5, 0x64, 0xfa, 0xc7, 0x3b, 0xba, 0xdf, 0x9c, 0x07, 0x70, 0xed, 0x4a, + 0xb1, 0x24, 0xdd, 0xc8, 0xa3, 0x7f, 0xb2, 0xab, 0x9d, 0xa3, 0x5e, 0x5c, 0xf3, 0x6f, 0x5f, 0x7e, + 0x05, 0x00, 0x00, 0xff, 0xff, 0x30, 0x48, 0x25, 0x2d, 0x0d, 0x03, 0x00, 0x00, +} diff --git a/store/service/proto/store.pb.micro.go b/store/service/proto/store.pb.micro.go new file mode 100644 index 00000000..422387b9 --- /dev/null +++ b/store/service/proto/store.pb.micro.go @@ -0,0 +1,207 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: micro/go-micro/store/service/proto/store.proto + +package go_micro_store + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +import ( + context "context" + client "github.com/micro/go-micro/client" + server "github.com/micro/go-micro/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// 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 + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ client.Option +var _ server.Option + +// Client API for Store service + +type StoreService interface { + List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (Store_ListService, error) + Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) + Write(ctx context.Context, in *WriteRequest, opts ...client.CallOption) (*WriteResponse, error) + Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) +} + +type storeService struct { + c client.Client + name string +} + +func NewStoreService(name string, c client.Client) StoreService { + if c == nil { + c = client.NewClient() + } + if len(name) == 0 { + name = "go.micro.store" + } + return &storeService{ + c: c, + name: name, + } +} + +func (c *storeService) List(ctx context.Context, in *ListRequest, opts ...client.CallOption) (Store_ListService, error) { + req := c.c.NewRequest(c.name, "Store.List", &ListRequest{}) + stream, err := c.c.Stream(ctx, req, opts...) + if err != nil { + return nil, err + } + if err := stream.Send(in); err != nil { + return nil, err + } + return &storeServiceList{stream}, nil +} + +type Store_ListService interface { + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Recv() (*ListResponse, error) +} + +type storeServiceList struct { + stream client.Stream +} + +func (x *storeServiceList) Close() error { + return x.stream.Close() +} + +func (x *storeServiceList) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *storeServiceList) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *storeServiceList) Recv() (*ListResponse, error) { + m := new(ListResponse) + err := x.stream.Recv(m) + if err != nil { + return nil, err + } + return m, nil +} + +func (c *storeService) Read(ctx context.Context, in *ReadRequest, opts ...client.CallOption) (*ReadResponse, error) { + req := c.c.NewRequest(c.name, "Store.Read", in) + out := new(ReadResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeService) Write(ctx context.Context, in *WriteRequest, opts ...client.CallOption) (*WriteResponse, error) { + req := c.c.NewRequest(c.name, "Store.Write", in) + out := new(WriteResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storeService) Delete(ctx context.Context, in *DeleteRequest, opts ...client.CallOption) (*DeleteResponse, error) { + req := c.c.NewRequest(c.name, "Store.Delete", in) + out := new(DeleteResponse) + err := c.c.Call(ctx, req, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Store service + +type StoreHandler interface { + List(context.Context, *ListRequest, Store_ListStream) error + Read(context.Context, *ReadRequest, *ReadResponse) error + Write(context.Context, *WriteRequest, *WriteResponse) error + Delete(context.Context, *DeleteRequest, *DeleteResponse) error +} + +func RegisterStoreHandler(s server.Server, hdlr StoreHandler, opts ...server.HandlerOption) error { + type store interface { + List(ctx context.Context, stream server.Stream) error + Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error + Write(ctx context.Context, in *WriteRequest, out *WriteResponse) error + Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error + } + type Store struct { + store + } + h := &storeHandler{hdlr} + return s.Handle(s.NewHandler(&Store{h}, opts...)) +} + +type storeHandler struct { + StoreHandler +} + +func (h *storeHandler) List(ctx context.Context, stream server.Stream) error { + m := new(ListRequest) + if err := stream.Recv(m); err != nil { + return err + } + return h.StoreHandler.List(ctx, m, &storeListStream{stream}) +} + +type Store_ListStream interface { + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Send(*ListResponse) error +} + +type storeListStream struct { + stream server.Stream +} + +func (x *storeListStream) Close() error { + return x.stream.Close() +} + +func (x *storeListStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *storeListStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *storeListStream) Send(m *ListResponse) error { + return x.stream.Send(m) +} + +func (h *storeHandler) Read(ctx context.Context, in *ReadRequest, out *ReadResponse) error { + return h.StoreHandler.Read(ctx, in, out) +} + +func (h *storeHandler) Write(ctx context.Context, in *WriteRequest, out *WriteResponse) error { + return h.StoreHandler.Write(ctx, in, out) +} + +func (h *storeHandler) Delete(ctx context.Context, in *DeleteRequest, out *DeleteResponse) error { + return h.StoreHandler.Delete(ctx, in, out) +} diff --git a/store/service/proto/store.proto b/store/service/proto/store.proto new file mode 100644 index 00000000..5f9ea233 --- /dev/null +++ b/store/service/proto/store.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package go.micro.store; + +service Store { + rpc List(ListRequest) returns (stream ListResponse) {}; + rpc Read(ReadRequest) returns (ReadResponse) {}; + rpc Write(WriteRequest) returns (WriteResponse) {}; + rpc Delete(DeleteRequest) returns (DeleteResponse) {}; +} + +message Record { + // key of the record + string key = 1; + // value in the record + bytes value = 2; + // timestamp in unix seconds + int64 expiry = 3; +} + +message ReadRequest { + repeated string keys = 1; +} + +message ReadResponse { + repeated Record records = 1; +} + +message WriteRequest { + repeated Record records = 2; +} + +message WriteResponse {} + +message DeleteRequest { + repeated string keys = 1; +} + +message DeleteResponse {} + +message ListRequest { + // optional key + string key = 1; +} + +message ListResponse { + repeated Record records = 1; +} diff --git a/store/service/service.go b/store/service/service.go new file mode 100644 index 00000000..1cb486bc --- /dev/null +++ b/store/service/service.go @@ -0,0 +1,120 @@ +// Package service implements the store service interface +package service + +import ( + "context" + "io" + "time" + + "github.com/micro/go-micro/client" + "github.com/micro/go-micro/config/options" + "github.com/micro/go-micro/store" + pb "github.com/micro/go-micro/store/service/proto" +) + +type serviceStore struct { + options.Options + + // Addresses of the nodes + Nodes []string + + // store service client + Client pb.StoreService +} + +// Sync all the known records +func (s *serviceStore) List() ([]*store.Record, error) { + stream, err := s.Client.List(context.Background(), &pb.ListRequest{}, client.WithAddress(s.Nodes...)) + if err != nil { + return nil, err + } + defer stream.Close() + + var records []*store.Record + + for { + rsp, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return records, err + } + for _, record := range rsp.Records { + records = append(records, &store.Record{ + Key: record.Key, + Value: record.Value, + Expiry: time.Duration(record.Expiry) * time.Second, + }) + } + } + + return records, nil +} + +// Read a record with key +func (s *serviceStore) Read(keys ...string) ([]*store.Record, error) { + rsp, err := s.Client.Read(context.Background(), &pb.ReadRequest{ + Keys: keys, + }, client.WithAddress(s.Nodes...)) + if err != nil { + return nil, err + } + + records := make([]*store.Record, 0, len(rsp.Records)) + for _, val := range rsp.Records { + records = append(records, &store.Record{ + Key: val.Key, + Value: val.Value, + Expiry: time.Duration(val.Expiry) * time.Second, + }) + } + return records, nil +} + +// Write a record +func (s *serviceStore) Write(recs ...*store.Record) error { + records := make([]*pb.Record, 0, len(recs)) + + for _, record := range recs { + records = append(records, &pb.Record{ + Key: record.Key, + Value: record.Value, + Expiry: int64(record.Expiry.Seconds()), + }) + } + + _, err := s.Client.Write(context.Background(), &pb.WriteRequest{ + Records: records, + }, client.WithAddress(s.Nodes...)) + + return err +} + +// Delete a record with key +func (s *serviceStore) Delete(keys ...string) error { + _, err := s.Client.Delete(context.Background(), &pb.DeleteRequest{ + Keys: keys, + }, client.WithAddress(s.Nodes...)) + return err +} + +// NewStore returns a new store service implementation +func NewStore(opts ...options.Option) store.Store { + options := options.NewOptions(opts...) + + var nodes []string + + n, ok := options.Values().Get("store.nodes") + if ok { + nodes = n.([]string) + } + + service := &serviceStore{ + Options: options, + Nodes: nodes, + Client: pb.NewStoreService("go.micro.store", client.DefaultClient), + } + + return service +} diff --git a/data/store/store.go b/store/store.go similarity index 52% rename from data/store/store.go rename to store/store.go index e7241591..9d265500 100644 --- a/data/store/store.go +++ b/store/store.go @@ -4,26 +4,23 @@ package store import ( "errors" "time" - - "github.com/micro/go-micro/config/options" ) var ( + // ErrNotFound is returned when a Read key doesn't exist ErrNotFound = errors.New("not found") ) // Store is a data storage interface type Store interface { - // embed options - options.Options - // Dump the known records - Dump() ([]*Record, error) - // Read a record with key - Read(key string) (*Record, error) - // Write a record - Write(r *Record) error - // Delete a record with key - Delete(key string) error + // List all the known records + List() ([]*Record, error) + // Read records with keys + Read(key ...string) ([]*Record, error) + // Write records + Write(rec ...*Record) error + // Delete records with keys + Delete(key ...string) error } // Record represents a data record diff --git a/sync/cron.go b/sync/cron.go index f5dcdb0f..0fd479a1 100644 --- a/sync/cron.go +++ b/sync/cron.go @@ -5,7 +5,7 @@ import ( "math" "time" - "github.com/micro/go-micro/sync/leader/consul" + "github.com/micro/go-micro/sync/leader/etcd" "github.com/micro/go-micro/sync/task" "github.com/micro/go-micro/sync/task/local" "github.com/micro/go-micro/util/log" @@ -80,7 +80,7 @@ func NewCron(opts ...Option) Cron { } if options.Leader == nil { - options.Leader = consul.NewLeader() + options.Leader = etcd.NewLeader() } if options.Task == nil { diff --git a/sync/leader/consul/consul.go b/sync/leader/consul/consul.go deleted file mode 100644 index b5b344c6..00000000 --- a/sync/leader/consul/consul.go +++ /dev/null @@ -1,158 +0,0 @@ -package consul - -import ( - "fmt" - "log" - "net" - "os" - "path" - "sync" - - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/api/watch" - "github.com/micro/go-micro/sync/leader" -) - -type consulLeader struct { - opts leader.Options - c *api.Client -} - -type consulElected struct { - c *api.Client - l *api.Lock - id string - key string - opts leader.ElectOptions - - mtx sync.RWMutex - rv <-chan struct{} -} - -func (c *consulLeader) Elect(id string, opts ...leader.ElectOption) (leader.Elected, error) { - var options leader.ElectOptions - for _, o := range opts { - o(&options) - } - - key := path.Join("micro/leader", c.opts.Group) - - lc, err := c.c.LockOpts(&api.LockOptions{ - Key: key, - Value: []byte(id), - }) - if err != nil { - return nil, err - } - - rv, err := lc.Lock(nil) - if err != nil { - return nil, err - } - - return &consulElected{ - c: c.c, - key: key, - rv: rv, - id: id, - l: lc, - opts: options, - }, nil -} - -func (c *consulLeader) Follow() chan string { - ch := make(chan string, 1) - - key := path.Join("/micro/leader", c.opts.Group) - - p, err := watch.Parse(map[string]interface{}{ - "type": "key", - "key": key, - }) - if err != nil { - return ch - } - p.Handler = func(idx uint64, raw interface{}) { - if raw == nil { - return // ignore - } - v, ok := raw.(*api.KVPair) - if !ok || v == nil { - return // ignore - } - ch <- string(v.Value) - } - - go p.RunWithClientAndLogger(c.c, log.New(os.Stdout, "consul: ", log.Lshortfile)) - return ch -} - -func (c *consulLeader) String() string { - return "consul" -} - -func (c *consulElected) Id() string { - return c.id -} - -func (c *consulElected) Reelect() error { - rv, err := c.l.Lock(nil) - if err != nil { - return err - } - - c.mtx.Lock() - c.rv = rv - c.mtx.Unlock() - return nil -} - -func (c *consulElected) Revoked() chan bool { - ch := make(chan bool, 1) - c.mtx.RLock() - rv := c.rv - c.mtx.RUnlock() - - go func() { - <-rv - ch <- true - close(ch) - }() - - return ch -} - -func (c *consulElected) Resign() error { - return c.l.Unlock() -} - -func NewLeader(opts ...leader.Option) leader.Leader { - options := leader.Options{ - Group: "default", - } - for _, o := range opts { - o(&options) - } - - config := api.DefaultConfig() - - // set host - // config.Host something - // check if there are any addrs - if len(options.Nodes) > 0 { - addr, port, err := net.SplitHostPort(options.Nodes[0]) - if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { - port = "8500" - config.Address = fmt.Sprintf("%s:%s", addr, port) - } else if err == nil { - config.Address = fmt.Sprintf("%s:%s", addr, port) - } - } - - client, _ := api.NewClient(config) - - return &consulLeader{ - opts: options, - c: client, - } -} diff --git a/sync/leader/etcd/etcd.go b/sync/leader/etcd/etcd.go new file mode 100644 index 00000000..f8e5baa5 --- /dev/null +++ b/sync/leader/etcd/etcd.go @@ -0,0 +1,136 @@ +package etcd + +import ( + "context" + "log" + "path" + "strings" + + client "github.com/coreos/etcd/clientv3" + cc "github.com/coreos/etcd/clientv3/concurrency" + "github.com/micro/go-micro/sync/leader" +) + +type etcdLeader struct { + opts leader.Options + path string + client *client.Client +} + +type etcdElected struct { + s *cc.Session + e *cc.Election + id string +} + +func (e *etcdLeader) Elect(id string, opts ...leader.ElectOption) (leader.Elected, error) { + var options leader.ElectOptions + for _, o := range opts { + o(&options) + } + + // make path + path := path.Join(e.path, strings.Replace(id, "/", "-", -1)) + + s, err := cc.NewSession(e.client) + if err != nil { + return nil, err + } + + l := cc.NewElection(s, path) + + if err := l.Campaign(context.TODO(), id); err != nil { + return nil, err + } + + return &etcdElected{ + e: l, + id: id, + }, nil +} + +func (e *etcdLeader) Follow() chan string { + ch := make(chan string) + + s, err := cc.NewSession(e.client) + if err != nil { + return ch + } + + l := cc.NewElection(s, e.path) + ech := l.Observe(context.Background()) + + go func() { + for r := range ech { + ch <- string(r.Kvs[0].Value) + } + }() + + return ch +} + +func (e *etcdLeader) String() string { + return "etcd" +} + +func (e *etcdElected) Reelect() error { + return e.e.Campaign(context.TODO(), e.id) +} + +func (e *etcdElected) Revoked() chan bool { + ch := make(chan bool, 1) + ech := e.e.Observe(context.Background()) + + go func() { + for r := range ech { + if string(r.Kvs[0].Value) != e.id { + ch <- true + close(ch) + return + } + } + }() + + return ch +} + +func (e *etcdElected) Resign() error { + return e.e.Resign(context.Background()) +} + +func (e *etcdElected) Id() string { + return e.id +} + +func NewLeader(opts ...leader.Option) leader.Leader { + var options leader.Options + for _, o := range opts { + o(&options) + } + + var endpoints []string + + for _, addr := range options.Nodes { + if len(addr) > 0 { + endpoints = append(endpoints, addr) + } + } + + if len(endpoints) == 0 { + endpoints = []string{"http://127.0.0.1:2379"} + } + + // TODO: parse addresses + c, err := client.New(client.Config{ + Endpoints: endpoints, + }) + if err != nil { + log.Fatal(err) + } + + return &etcdLeader{ + path: "/micro/leader", + client: c, + opts: options, + } +} diff --git a/sync/lock/consul/consul.go b/sync/lock/consul/consul.go deleted file mode 100644 index ef0f2dd4..00000000 --- a/sync/lock/consul/consul.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package consul is a consul implemenation of lock -package consul - -import ( - "errors" - "fmt" - "net" - "sync" - "time" - - "github.com/hashicorp/consul/api" - lock "github.com/micro/go-micro/sync/lock" -) - -type consulLock struct { - sync.Mutex - - locks map[string]*api.Lock - opts lock.Options - c *api.Client -} - -func (c *consulLock) Acquire(id string, opts ...lock.AcquireOption) error { - var options lock.AcquireOptions - for _, o := range opts { - o(&options) - } - - if options.Wait <= time.Duration(0) { - options.Wait = api.DefaultLockWaitTime - } - - ttl := fmt.Sprintf("%v", options.TTL) - if options.TTL <= time.Duration(0) { - ttl = api.DefaultLockSessionTTL - } - - l, err := c.c.LockOpts(&api.LockOptions{ - Key: c.opts.Prefix + id, - LockWaitTime: options.Wait, - SessionTTL: ttl, - }) - - if err != nil { - return err - } - - _, err = l.Lock(nil) - if err != nil { - return err - } - - c.Lock() - c.locks[id] = l - c.Unlock() - - return nil -} - -func (c *consulLock) Release(id string) error { - c.Lock() - defer c.Unlock() - l, ok := c.locks[id] - if !ok { - return errors.New("lock not found") - } - err := l.Unlock() - delete(c.locks, id) - return err -} - -func (c *consulLock) String() string { - return "consul" -} - -func NewLock(opts ...lock.Option) lock.Lock { - var options lock.Options - for _, o := range opts { - o(&options) - } - - config := api.DefaultConfig() - - // set host - // config.Host something - // check if there are any addrs - if len(options.Nodes) > 0 { - addr, port, err := net.SplitHostPort(options.Nodes[0]) - if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { - port = "8500" - config.Address = fmt.Sprintf("%s:%s", options.Nodes[0], port) - } else if err == nil { - config.Address = fmt.Sprintf("%s:%s", addr, port) - } - } - - client, _ := api.NewClient(config) - - return &consulLock{ - locks: make(map[string]*api.Lock), - opts: options, - c: client, - } -} diff --git a/sync/lock/etcd/etcd.go b/sync/lock/etcd/etcd.go new file mode 100644 index 00000000..9c55ddde --- /dev/null +++ b/sync/lock/etcd/etcd.go @@ -0,0 +1,113 @@ +// Package etcd is an etcd implementation of lock +package etcd + +import ( + "context" + "errors" + "log" + "path" + "strings" + "sync" + + client "github.com/coreos/etcd/clientv3" + cc "github.com/coreos/etcd/clientv3/concurrency" + "github.com/micro/go-micro/sync/lock" +) + +type etcdLock struct { + opts lock.Options + path string + client *client.Client + + sync.Mutex + locks map[string]*elock +} + +type elock struct { + s *cc.Session + m *cc.Mutex +} + +func (e *etcdLock) Acquire(id string, opts ...lock.AcquireOption) error { + var options lock.AcquireOptions + for _, o := range opts { + o(&options) + } + + // make path + path := path.Join(e.path, strings.Replace(e.opts.Prefix+id, "/", "-", -1)) + + var sopts []cc.SessionOption + if options.TTL > 0 { + sopts = append(sopts, cc.WithTTL(int(options.TTL.Seconds()))) + } + + s, err := cc.NewSession(e.client, sopts...) + if err != nil { + return err + } + + m := cc.NewMutex(s, path) + + if err := m.Lock(context.TODO()); err != nil { + return err + } + + e.Lock() + e.locks[id] = &elock{ + s: s, + m: m, + } + e.Unlock() + return nil +} + +func (e *etcdLock) Release(id string) error { + e.Lock() + defer e.Unlock() + v, ok := e.locks[id] + if !ok { + return errors.New("lock not found") + } + err := v.m.Unlock(context.Background()) + delete(e.locks, id) + return err +} + +func (e *etcdLock) String() string { + return "etcd" +} + +func NewLock(opts ...lock.Option) lock.Lock { + var options lock.Options + for _, o := range opts { + o(&options) + } + + var endpoints []string + + for _, addr := range options.Nodes { + if len(addr) > 0 { + endpoints = append(endpoints, addr) + } + } + + if len(endpoints) == 0 { + endpoints = []string{"http://127.0.0.1:2379"} + } + + // TODO: parse addresses + c, err := client.New(client.Config{ + Endpoints: endpoints, + }) + if err != nil { + log.Fatal(err) + } + + return &etcdLock{ + path: "/micro/lock", + client: c, + opts: options, + locks: make(map[string]*elock), + } +} diff --git a/sync/lock/http/http.go b/sync/lock/http/http.go new file mode 100644 index 00000000..497774d5 --- /dev/null +++ b/sync/lock/http/http.go @@ -0,0 +1,135 @@ +// Package http adds a http lock implementation +package http + +import ( + "errors" + "fmt" + "hash/crc32" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/micro/go-micro/sync/lock" +) + +var ( + DefaultPath = "/sync/lock" + DefaultAddress = "localhost:8080" +) + +type httpLock struct { + opts lock.Options +} + +func (h *httpLock) url(do, id string) (string, error) { + sum := crc32.ChecksumIEEE([]byte(id)) + node := h.opts.Nodes[sum%uint32(len(h.opts.Nodes))] + + // parse the host:port or whatever + uri, err := url.Parse(node) + if err != nil { + return "", err + } + + if len(uri.Scheme) == 0 { + uri.Scheme = "http" + } + + // set path + // build path + path := filepath.Join(DefaultPath, do, h.opts.Prefix, id) + uri.Path = path + + // return url + return uri.String(), nil +} + +func (h *httpLock) Acquire(id string, opts ...lock.AcquireOption) error { + var options lock.AcquireOptions + for _, o := range opts { + o(&options) + } + + uri, err := h.url("acquire", id) + if err != nil { + return err + } + + ttl := fmt.Sprintf("%d", int64(options.TTL.Seconds())) + wait := fmt.Sprintf("%d", int64(options.Wait.Seconds())) + + rsp, err := http.PostForm(uri, url.Values{ + "id": {id}, + "ttl": {ttl}, + "wait": {wait}, + }) + if err != nil { + return err + } + defer rsp.Body.Close() + + b, err := ioutil.ReadAll(rsp.Body) + if err != nil { + return err + } + + // success + if rsp.StatusCode == 200 { + return nil + } + + // return error + return errors.New(string(b)) +} + +func (h *httpLock) Release(id string) error { + uri, err := h.url("release", id) + if err != nil { + return err + } + + vals := url.Values{ + "id": {id}, + } + + req, err := http.NewRequest("DELETE", uri, strings.NewReader(vals.Encode())) + if err != nil { + return err + } + + rsp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer rsp.Body.Close() + + b, err := ioutil.ReadAll(rsp.Body) + if err != nil { + return err + } + + // success + if rsp.StatusCode == 200 { + return nil + } + + // return error + return errors.New(string(b)) +} + +func NewLock(opts ...lock.Option) lock.Lock { + var options lock.Options + for _, o := range opts { + o(&options) + } + + if len(options.Nodes) == 0 { + options.Nodes = []string{DefaultAddress} + } + + return &httpLock{ + opts: options, + } +} diff --git a/sync/lock/http/server/server.go b/sync/lock/http/server/server.go new file mode 100644 index 00000000..49298547 --- /dev/null +++ b/sync/lock/http/server/server.go @@ -0,0 +1,45 @@ +// Package server implements the sync http server +package server + +import ( + "net/http" + + "github.com/micro/go-micro/sync/lock" + lkhttp "github.com/micro/go-micro/sync/lock/http" +) + +func Handler(lk lock.Lock) http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc(lkhttp.DefaultPath, func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + id := r.Form.Get("id") + if len(id) == 0 { + return + } + + switch r.Method { + case "POST": + err := lk.Acquire(id) + if err != nil { + http.Error(w, err.Error(), 500) + } + case "DELETE": + err := lk.Release(id) + if err != nil { + http.Error(w, err.Error(), 500) + } + } + }) + + return mux +} + +func Server(lk lock.Lock) *http.Server { + server := &http.Server{ + Addr: lkhttp.DefaultAddress, + Handler: Handler(lk), + } + return server +} diff --git a/sync/lock/lock.go b/sync/lock/lock.go index 8be6629f..c21a9125 100644 --- a/sync/lock/lock.go +++ b/sync/lock/lock.go @@ -2,9 +2,14 @@ package lock import ( + "errors" "time" ) +var ( + ErrLockTimeout = errors.New("lock timeout") +) + // Lock is a distributed locking interface type Lock interface { // Acquire a lock with given id diff --git a/sync/lock/memory/memory.go b/sync/lock/memory/memory.go new file mode 100644 index 00000000..f8e46c7f --- /dev/null +++ b/sync/lock/memory/memory.go @@ -0,0 +1,142 @@ +// Package memory provides a sync.Mutex implementation of the lock for local use +package memory + +import ( + "sync" + "time" + + lock "github.com/micro/go-micro/sync/lock" +) + +type memoryLock struct { + sync.RWMutex + locks map[string]*mlock +} + +type mlock struct { + id string + time time.Time + ttl time.Duration + release chan bool +} + +func (m *memoryLock) Acquire(id string, opts ...lock.AcquireOption) error { + // lock our access + m.Lock() + + var options lock.AcquireOptions + for _, o := range opts { + o(&options) + } + + lk, ok := m.locks[id] + if !ok { + m.locks[id] = &mlock{ + id: id, + time: time.Now(), + ttl: options.TTL, + release: make(chan bool), + } + // unlock + m.Unlock() + return nil + } + + m.Unlock() + + // set wait time + var wait <-chan time.Time + var ttl <-chan time.Time + + // decide if we should wait + if options.Wait > time.Duration(0) { + wait = time.After(options.Wait) + } + + // check the ttl of the lock + if lk.ttl > time.Duration(0) { + // time lived for the lock + live := time.Since(lk.time) + + // set a timer for the leftover ttl + if live > lk.ttl { + // release the lock if it expired + _ = m.Release(id) + } else { + ttl = time.After(live) + } + } + +lockLoop: + for { + // wait for the lock to be released + select { + case <-lk.release: + m.Lock() + + // someone locked before us + lk, ok = m.locks[id] + if ok { + m.Unlock() + continue + } + + // got chance to lock + m.locks[id] = &mlock{ + id: id, + time: time.Now(), + ttl: options.TTL, + release: make(chan bool), + } + + m.Unlock() + + break lockLoop + case <-ttl: + // ttl exceeded + _ = m.Release(id) + // TODO: check the ttl again above + ttl = nil + // try acquire + continue + case <-wait: + return lock.ErrLockTimeout + } + } + + return nil +} + +func (m *memoryLock) Release(id string) error { + m.Lock() + defer m.Unlock() + + lk, ok := m.locks[id] + // no lock exists + if !ok { + return nil + } + + // delete the lock + delete(m.locks, id) + + select { + case <-lk.release: + return nil + default: + close(lk.release) + } + + return nil +} + +func NewLock(opts ...lock.Option) lock.Lock { + var options lock.Options + for _, o := range opts { + o(&options) + } + + return &memoryLock{ + locks: make(map[string]*mlock), + } +} diff --git a/sync/map.go b/sync/map.go index 5f7865e9..2515d57c 100644 --- a/sync/map.go +++ b/sync/map.go @@ -5,10 +5,11 @@ import ( "encoding/base64" "encoding/json" "fmt" + "sort" - "github.com/micro/go-micro/data/store" - ckv "github.com/micro/go-micro/data/store/consul" - lock "github.com/micro/go-micro/sync/lock/consul" + "github.com/micro/go-micro/store" + ckv "github.com/micro/go-micro/store/etcd" + lock "github.com/micro/go-micro/sync/lock/etcd" ) type syncMap struct { @@ -39,8 +40,12 @@ func (m *syncMap) Read(key, val interface{}) error { return err } + if len(kval) == 0 { + return store.ErrNotFound + } + // decode value - return json.Unmarshal(kval.Value, val) + return json.Unmarshal(kval[0].Value, val) } func (m *syncMap) Write(key, val interface{}) error { @@ -85,11 +90,15 @@ func (m *syncMap) Delete(key interface{}) error { } func (m *syncMap) Iterate(fn func(key, val interface{}) error) error { - keyvals, err := m.opts.Store.Dump() + keyvals, err := m.opts.Store.List() if err != nil { return err } + sort.Slice(keyvals, func(i, j int) bool { + return keyvals[i].Key < keyvals[j].Key + }) + for _, keyval := range keyvals { // lock if err := m.opts.Lock.Acquire(keyval.Key); err != nil { diff --git a/sync/map_test.go b/sync/map_test.go new file mode 100644 index 00000000..fa54cd2c --- /dev/null +++ b/sync/map_test.go @@ -0,0 +1,40 @@ +package sync + +import ( + "testing" + "time" + + "github.com/micro/go-micro/store" + store_mock "github.com/micro/go-micro/store/mock" + mem_lock "github.com/micro/go-micro/sync/lock/memory" + "github.com/stretchr/testify/mock" +) + +func TestIterate(t *testing.T) { + recA := &store.Record{ + Key: "A", + Value: nil, + } + recB := &store.Record{ + Key: "B", + Value: nil, + } + s1 := &store_mock.Store{} + s2 := &store_mock.Store{} + s1.On("List").Return([]*store.Record{recA, recB}, nil) + s2.On("List").Return([]*store.Record{recB, recA}, nil) + s1.On("Write", mock.Anything).Return(nil) + s2.On("Write", mock.Anything).Return(nil) + + f := func(key, val interface{}) error { + time.Sleep(1 * time.Millisecond) + return nil + } + l := mem_lock.NewLock() + m1 := NewMap(WithStore(s1), WithLock(l)) + m2 := NewMap(WithStore(s2), WithLock(l)) + go func() { + m2.Iterate(f) + }() + m1.Iterate(f) +} diff --git a/sync/options.go b/sync/options.go index 65f20e28..0e5cf3d0 100644 --- a/sync/options.go +++ b/sync/options.go @@ -1,7 +1,7 @@ package sync import ( - "github.com/micro/go-micro/data/store" + "github.com/micro/go-micro/store" "github.com/micro/go-micro/sync/leader" "github.com/micro/go-micro/sync/lock" "github.com/micro/go-micro/sync/time" diff --git a/sync/sync.go b/sync/sync.go index 27a6104a..7b080b1d 100644 --- a/sync/sync.go +++ b/sync/sync.go @@ -2,7 +2,7 @@ package sync import ( - "github.com/micro/go-micro/data/store" + "github.com/micro/go-micro/store" "github.com/micro/go-micro/sync/leader" "github.com/micro/go-micro/sync/lock" "github.com/micro/go-micro/sync/task" diff --git a/sync/task/task.go b/sync/task/task.go index 031355ca..c9b9b5d6 100644 --- a/sync/task/task.go +++ b/sync/task/task.go @@ -63,7 +63,9 @@ func (s Schedule) Run() <-chan time.Time { } // start ticker - for t := range time.Tick(s.Interval) { + ticker := time.NewTicker(s.Interval) + defer ticker.Stop() + for t := range ticker.C { ch <- t } }() diff --git a/transport/grpc/handler.go b/transport/grpc/handler.go index 3220f7dd..e02e1445 100644 --- a/transport/grpc/handler.go +++ b/transport/grpc/handler.go @@ -3,6 +3,7 @@ package grpc import ( "runtime/debug" + "github.com/micro/go-micro/errors" "github.com/micro/go-micro/transport" pb "github.com/micro/go-micro/transport/grpc/proto" "github.com/micro/go-micro/util/log" @@ -16,6 +17,8 @@ type microTransport struct { } func (m *microTransport) Stream(ts pb.Transport_StreamServer) error { + var err error + sock := &grpcTransportSocket{ stream: ts, local: m.addr, @@ -30,10 +33,12 @@ func (m *microTransport) Stream(ts pb.Transport_StreamServer) error { if r := recover(); r != nil { log.Log(r, string(debug.Stack())) sock.Close() + err = errors.InternalServerError("go.micro.transport", "panic recovered: %v", r) } }() // execute socket func m.fn(sock) - return nil + + return err } diff --git a/transport/http_transport.go b/transport/http_transport.go index c9878073..1ce109aa 100644 --- a/transport/http_transport.go +++ b/transport/http_transport.go @@ -285,8 +285,14 @@ func (h *httpTransportSocket) Recv(m *Message) error { func (h *httpTransportSocket) Send(m *Message) error { if h.r.ProtoMajor == 1 { + // make copy of header + hdr := make(http.Header) + for k, v := range h.r.Header { + hdr[k] = v + } + rsp := &http.Response{ - Header: h.r.Header, + Header: hdr, Body: ioutil.NopCloser(bytes.NewReader(m.Body)), Status: "200 OK", StatusCode: 200, diff --git a/transport/quic/quic.go b/transport/quic/quic.go index 956c58f9..a39e9519 100644 --- a/transport/quic/quic.go +++ b/transport/quic/quic.go @@ -7,7 +7,7 @@ import ( "encoding/gob" "time" - "github.com/lucas-clemente/quic-go" + quic "github.com/lucas-clemente/quic-go" "github.com/micro/go-micro/transport" utls "github.com/micro/go-micro/util/tls" ) diff --git a/tunnel/broker/broker.go b/tunnel/broker/broker.go index 6778dfaa..d11e160c 100644 --- a/tunnel/broker/broker.go +++ b/tunnel/broker/broker.go @@ -58,7 +58,7 @@ func (t *tunBroker) Disconnect() error { func (t *tunBroker) Publish(topic string, m *broker.Message, opts ...broker.PublishOption) error { // TODO: this is probably inefficient, we might want to just maintain an open connection // it may be easier to add broadcast to the tunnel - c, err := t.tunnel.Dial(topic, tunnel.DialMulticast()) + c, err := t.tunnel.Dial(topic, tunnel.DialMode(tunnel.Multicast)) if err != nil { return err } @@ -71,7 +71,7 @@ func (t *tunBroker) Publish(topic string, m *broker.Message, opts ...broker.Publ } func (t *tunBroker) Subscribe(topic string, h broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { - l, err := t.tunnel.Listen(topic) + l, err := t.tunnel.Listen(topic, tunnel.ListenMode(tunnel.Multicast)) if err != nil { return nil, err } diff --git a/tunnel/crypto.go b/tunnel/crypto.go new file mode 100644 index 00000000..ba0d5057 --- /dev/null +++ b/tunnel/crypto.go @@ -0,0 +1,72 @@ +package tunnel + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "io" +) + +// hash hahes the data into 32 bytes key and returns it +// hash uses sha256 underneath to hash the supplied key +func hash(key string) []byte { + hasher := sha256.New() + hasher.Write([]byte(key)) + return hasher.Sum(nil) +} + +// Encrypt encrypts data and returns the encrypted data +func Encrypt(data []byte, key string) ([]byte, error) { + // generate a new AES cipher using our 32 byte key + c, err := aes.NewCipher(hash(key)) + if err != nil { + return nil, err + } + + // gcm or Galois/Counter Mode, is a mode of operation + // for symmetric key cryptographic block ciphers + // - https://en.wikipedia.org/wiki/Galois/Counter_Mode + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + // create a new byte array the size of the nonce + // NOTE: we might use smaller nonce size in the future + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // NOTE: we prepend the nonce to the payload + // we need to do this as we need the same nonce + // to decrypt the payload when receiving it + return gcm.Seal(nonce, nonce, data, nil), nil +} + +// Decrypt decrypts the payload and returns the decrypted data +func Decrypt(data []byte, key string) ([]byte, error) { + // generate AES cipher for decrypting the message + c, err := aes.NewCipher(hash(key)) + if err != nil { + return nil, err + } + + // we use GCM to encrypt the payload + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + // NOTE: we need to parse out nonce from the payload + // we prepend the nonce to every encrypted payload + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} diff --git a/tunnel/crypto_test.go b/tunnel/crypto_test.go new file mode 100644 index 00000000..6f8498b9 --- /dev/null +++ b/tunnel/crypto_test.go @@ -0,0 +1,41 @@ +package tunnel + +import ( + "bytes" + "testing" +) + +func TestEncrypt(t *testing.T) { + key := "tokenpassphrase" + data := []byte("supersecret") + + cipherText, err := Encrypt(data, key) + if err != nil { + t.Errorf("failed to encrypt data: %v", err) + } + + // verify the cipherText is not the same as data + if bytes.Equal(data, cipherText) { + t.Error("encrypted data are the same as plaintext") + } +} + +func TestDecrypt(t *testing.T) { + key := "tokenpassphrase" + data := []byte("supersecret") + + cipherText, err := Encrypt(data, key) + if err != nil { + t.Errorf("failed to encrypt data: %v", err) + } + + plainText, err := Decrypt(cipherText, key) + if err != nil { + t.Errorf("failed to decrypt data: %v", err) + } + + // verify the plainText is the same as data + if !bytes.Equal(data, plainText) { + t.Error("decrypted data not the same as plaintext") + } +} diff --git a/tunnel/default.go b/tunnel/default.go index 6107021e..a6cb08c9 100644 --- a/tunnel/default.go +++ b/tunnel/default.go @@ -30,7 +30,7 @@ type tun struct { // the unique id for this tunnel id string - // tunnel token for authentication + // tunnel token for session encryption token string // to indicate if we're connected or not @@ -90,8 +90,12 @@ func (t *tun) getSession(channel, session string) (*session, bool) { return s, ok } +// delSession deletes a session if it exists func (t *tun) delSession(channel, session string) { t.Lock() + if s, ok := t.sessions[channel+session]; ok { + s.Close() + } delete(t.sessions, channel+session) t.Unlock() } @@ -101,6 +105,7 @@ func (t *tun) listChannels() []string { t.RLock() defer t.RUnlock() + //nolint:prealloc var channels []string for _, session := range t.sessions { if session.session != "listener" { @@ -118,10 +123,10 @@ func (t *tun) newSession(channel, sessionId string) (*session, bool) { tunnel: t.id, channel: channel, session: sessionId, + token: t.token, closed: make(chan bool), recv: make(chan *message, 128), send: t.send, - wait: make(chan bool), errChan: make(chan error, 1), } @@ -146,6 +151,9 @@ func (t *tun) newSessionId() string { return uuid.New().String() } +// announce will send a message to the link to tell the other side of a channel mapping we have. +// This usually happens if someone calls Dial and sends a discover message but otherwise we +// periodically send these messages to asynchronously manage channel mappings. func (t *tun) announce(channel, session string, link *link) { // create the "announce" response message for a discover request msg := &transport.Message{ @@ -155,7 +163,6 @@ func (t *tun) announce(channel, session string, link *link) { "Micro-Tunnel-Channel": channel, "Micro-Tunnel-Session": session, "Micro-Tunnel-Link": link.id, - "Micro-Tunnel-Token": t.token, }, } @@ -190,8 +197,8 @@ func (t *tun) announce(channel, session string, link *link) { } } -// monitor monitors outbound links and attempts to reconnect to the failed ones -func (t *tun) monitor() { +// manage monitors outbound links and attempts to reconnect to the failed ones +func (t *tun) manage() { reconnect := time.NewTicker(ReconnectTime) defer reconnect.Stop() @@ -200,62 +207,121 @@ func (t *tun) monitor() { case <-t.closed: return case <-reconnect.C: - t.RLock() + t.manageLinks() + } + } +} - var delLinks []string - // check the link status and purge dead links - for node, link := range t.links { - // check link status - switch link.Status() { - case "closed": - delLinks = append(delLinks, node) - case "error": - delLinks = append(delLinks, node) - } +// manageLink sends channel discover requests periodically and +// keepalive messages to link +func (t *tun) manageLink(link *link) { + keepalive := time.NewTicker(KeepAliveTime) + defer keepalive.Stop() + discover := time.NewTicker(DiscoverTime) + defer discover.Stop() + + for { + select { + case <-t.closed: + return + case <-link.closed: + return + case <-discover.C: + // send a discovery message to all links + if err := t.sendMsg("discover", link); err != nil { + log.Debugf("Tunnel failed to send discover to link %s: %v", link.Remote(), err) } - - t.RUnlock() - - // delete the dead links - if len(delLinks) > 0 { - t.Lock() - for _, node := range delLinks { - log.Debugf("Tunnel deleting dead link for %s", node) - link := t.links[node] - link.Close() - delete(t.links, node) - } - t.Unlock() - } - - // check current link status - var connect []string - - // build list of unknown nodes to connect to - t.RLock() - for _, node := range t.options.Nodes { - if _, ok := t.links[node]; !ok { - connect = append(connect, node) - } - } - t.RUnlock() - - for _, node := range connect { - // create new link - link, err := t.setupLink(node) - if err != nil { - log.Debugf("Tunnel failed to setup node link to %s: %v", node, err) - continue - } - // save the link - t.Lock() - t.links[node] = link - t.Unlock() + case <-keepalive.C: + // send keepalive message + log.Debugf("Tunnel sending keepalive to link: %v", link.Remote()) + if err := t.sendMsg("keepalive", link); err != nil { + log.Debugf("Tunnel error sending keepalive to link %v: %v", link.Remote(), err) + t.delLink(link.Remote()) + return } } } } +// manageLinks is a function that can be called to immediately to link setup +func (t *tun) manageLinks() { + var delLinks []string + + t.RLock() + + // check the link status and purge dead links + for node, link := range t.links { + // check link status + switch link.State() { + case "closed": + delLinks = append(delLinks, node) + case "error": + delLinks = append(delLinks, node) + } + } + + t.RUnlock() + + // delete the dead links + if len(delLinks) > 0 { + t.Lock() + for _, node := range delLinks { + log.Debugf("Tunnel deleting dead link for %s", node) + if link, ok := t.links[node]; ok { + link.Close() + delete(t.links, node) + } + } + t.Unlock() + } + + // check current link status + var connect []string + + // build list of unknown nodes to connect to + t.RLock() + + for _, node := range t.options.Nodes { + if _, ok := t.links[node]; !ok { + connect = append(connect, node) + } + } + + t.RUnlock() + + var wg sync.WaitGroup + + for _, node := range connect { + wg.Add(1) + + go func(node string) { + defer wg.Done() + + // create new link + link, err := t.setupLink(node) + if err != nil { + log.Debugf("Tunnel failed to setup node link to %s: %v", node, err) + return + } + + // save the link + t.Lock() + defer t.Unlock() + + // just check nothing else was setup in the interim + if _, ok := t.links[node]; ok { + link.Close() + return + } + // save the link + t.links[node] = link + }(node) + } + + // wait for all threads to finish + wg.Wait() +} + // process outgoing messages sent by all local sessions func (t *tun) process() { // manage the send buffer @@ -287,9 +353,6 @@ func (t *tun) process() { // set the session id newMsg.Header["Micro-Tunnel-Session"] = msg.session - // set the tunnel token - newMsg.Header["Micro-Tunnel-Token"] = t.token - // send the message via the interface t.RLock() @@ -303,8 +366,16 @@ func (t *tun) process() { // build the list of links ot send to for node, link := range t.links { + // get the values we need + link.RLock() + id := link.id + connected := link.connected + loopback := link.loopback + _, exists := link.channels[msg.channel] + link.RUnlock() + // if the link is not connected skip it - if !link.connected { + if !connected { log.Debugf("Link for node %s not connected", node) err = errors.New("link not connected") continue @@ -313,32 +384,31 @@ func (t *tun) process() { // if the link was a loopback accepted connection // and the message is being sent outbound via // a dialled connection don't use this link - if link.loopback && msg.outbound { + if loopback && msg.outbound { + log.Tracef("Link for node %s is loopback", node) err = errors.New("link is loopback") continue } // if the message was being returned by the loopback listener // send it back up the loopback link only - if msg.loopback && !link.loopback { + if msg.loopback && !loopback { + log.Tracef("Link for message %s is loopback", node) err = errors.New("link is not loopback") continue } // check the multicast mappings - if msg.multicast { - link.RLock() - _, ok := link.channels[msg.channel] - link.RUnlock() + if msg.mode == Multicast { // channel mapping not found in link - if !ok { + if !exists { continue } } else { // if we're picking the link check the id // this is where we explicitly set the link // in a message received via the listen method - if len(msg.link) > 0 && link.id != msg.link { + if len(msg.link) > 0 && id != msg.link { err = errors.New("link not found") continue } @@ -353,7 +423,7 @@ func (t *tun) process() { // send the message for _, link := range sendTo { // send the message via the current link - log.Debugf("Sending %+v to %s", newMsg.Header, link.Remote()) + log.Tracef("Tunnel sending %+v to %s", newMsg.Header, link.Remote()) if errr := link.Send(newMsg); errr != nil { log.Debugf("Tunnel error sending %+v to %s: %v", newMsg.Header, link.Remote(), errr) @@ -366,7 +436,7 @@ func (t *tun) process() { sent = true // keep sending broadcast messages - if msg.broadcast || msg.multicast { + if msg.mode > Unicast { continue } @@ -422,23 +492,26 @@ func (t *tun) listen(link *link) { // let us know if its a loopback var loopback bool + var connected bool + + // set the connected value + link.RLock() + connected = link.connected + link.RUnlock() for { // process anything via the net interface msg := new(transport.Message) if err := link.Recv(msg); err != nil { - log.Debugf("Tunnel link %s receive error: %#v", link.Remote(), err) + log.Debugf("Tunnel link %s receive error: %v", link.Remote(), err) return } - // always ensure we have the correct auth token - // TODO: segment the tunnel based on token - // e.g use it as the basis - token := msg.Header["Micro-Tunnel-Token"] - if token != t.token { - log.Debugf("Tunnel link %s received invalid token %s", token) - return - } + // TODO: figure out network authentication + // for now we use tunnel token to encrypt/decrypt + // session communication, but we will probably need + // some sort of network authentication (token) to avoid + // having rogue actors spamming the network // message type mtype := msg.Header["Micro-Tunnel"] @@ -451,17 +524,21 @@ func (t *tun) listen(link *link) { // if its not connected throw away the link // the first message we process needs to be connect - if !link.connected && mtype != "connect" { + if !connected && mtype != "connect" { log.Debugf("Tunnel link %s not connected", link.id) return } + // this state machine block handles the only message types + // that we know or care about; connect, close, open, accept, + // discover, announce, session, keepalive switch mtype { case "connect": log.Debugf("Tunnel link %s received connect message", link.Remote()) link.Lock() - // are we connecting to ourselves? + + // check if we're connecting to ourselves? if id == t.id { link.loopback = true loopback = true @@ -471,6 +548,8 @@ func (t *tun) listen(link *link) { link.id = link.Remote() // set as connected link.connected = true + connected = true + link.Unlock() // save the link once connected @@ -478,42 +557,46 @@ func (t *tun) listen(link *link) { t.links[link.Remote()] = link t.Unlock() - // send back a discovery + // send back an announcement of our channels discovery go t.announce("", "", link) + // ask for the things on the other wise + go t.sendMsg("discover", link) // nothing more to do continue case "close": - // TODO: handle the close message - // maybe report io.EOF or kill the link - - // close the link entirely + log.Debugf("Tunnel link %s received close message", link.Remote()) + // if there is no channel then we close the link + // as its a signal from the other side to close the connection if len(channel) == 0 { log.Debugf("Tunnel link %s received close message", link.Remote()) return } - // the entire listener was closed so remove it from the mapping + // the entire listener was closed by the remote side so we need to + // remove the channel mapping for it. should we also close sessions? if sessionId == "listener" { - link.Lock() - delete(link.channels, channel) - link.Unlock() + link.delChannel(channel) continue } + // assuming there's a channel and session // try get the dialing socket s, exists := t.getSession(channel, sessionId) - if exists { - // close and continue - s.Close() - continue + if exists && !loopback { + // only delete the session if its unicast + // otherwise ignore close on the multicast + if s.mode == Unicast { + // only delete this if its unicast + // but not if its a loopback conn + t.delSession(channel, sessionId) + continue + } } // otherwise its a session mapping of sorts case "keepalive": log.Debugf("Tunnel link %s received keepalive", link.Remote()) - link.Lock() // save the keepalive - link.lastKeepAlive = time.Now() - link.Unlock() + link.keepalive() continue // a new connection dialled outbound case "open": @@ -522,32 +605,35 @@ func (t *tun) listen(link *link) { // an accept returned by the listener case "accept": s, exists := t.getSession(channel, sessionId) - // we don't need this - if exists && s.multicast { + // just set accepted on anything not unicast + if exists && s.mode > Unicast { s.accepted = true continue } + // if its already accepted move on if exists && s.accepted { continue } + // otherwise we're going to process to accept // a continued session case "session": // process message - log.Debugf("Received %+v from %s", msg.Header, link.Remote()) + log.Tracef("Tunnel received %+v from %s", msg.Header, link.Remote()) // an announcement of a channel listener case "announce": + log.Tracef("Tunnel received %+v from %s", msg.Header, link.Remote()) + // process the announcement channels := strings.Split(channel, ",") // update mapping in the link - link.Lock() - for _, channel := range channels { - link.channels[channel] = time.Now() - } - link.Unlock() + link.setChannel(channels...) // this was an announcement not intended for anything - if sessionId == "listener" || sessionId == "" { + // if the dialing side sent "discover" then a session + // id would be present. We skip in case of multicast. + switch sessionId { + case "listener", "multicast", "": continue } @@ -559,14 +645,19 @@ func (t *tun) listen(link *link) { continue } - // send the announce back to the caller - s.recv <- &message{ + msg := &message{ typ: "announce", tunnel: id, channel: channel, session: sessionId, link: link.id, } + + // send the announce back to the caller + select { + case <-s.closed: + case s.recv <- msg: + } } continue case "discover": @@ -579,7 +670,7 @@ func (t *tun) listen(link *link) { } // strip tunnel message header - for k, _ := range msg.Header { + for k := range msg.Header { if strings.HasPrefix(k, "Micro-Tunnel") { delete(msg.Header, k) } @@ -603,7 +694,7 @@ func (t *tun) listen(link *link) { s, exists = t.getSession(channel, "listener") // only return accept to the session case mtype == "accept": - log.Debugf("Received accept message for %s %s", channel, sessionId) + log.Debugf("Tunnel received accept message for channel: %s session: %s", channel, sessionId) s, exists = t.getSession(channel, sessionId) if exists && s.accepted { continue @@ -623,7 +714,7 @@ func (t *tun) listen(link *link) { // bail if no session or listener has been found if !exists { - log.Debugf("Tunnel skipping no session %s %s exists", channel, sessionId) + log.Tracef("Tunnel skipping no channel: %s session: %s exists", channel, sessionId) // drop it, we don't care about // messages we don't know about continue @@ -636,22 +727,10 @@ func (t *tun) listen(link *link) { delete(t.sessions, channel) continue default: - // process + // otherwise process } - log.Debugf("Tunnel using channel %s session %s", s.channel, s.session) - - // is the session new? - select { - // if its new the session is actually blocked waiting - // for a connection. so we check if its waiting. - case <-s.wait: - // if its waiting e.g its new then we close it - default: - // set remote address of the session - s.remote = msg.Header["Remote"] - close(s.wait) - } + log.Tracef("Tunnel using channel: %s session: %s type: %s", s.channel, s.session, mtype) // construct a new transport message tmsg := &transport.Message{ @@ -665,6 +744,7 @@ func (t *tun) listen(link *link) { typ: mtype, channel: channel, session: sessionId, + mode: s.mode, data: tmsg, link: link.id, loopback: loopback, @@ -680,87 +760,39 @@ func (t *tun) listen(link *link) { } } -// discover sends channel discover requests periodically -func (t *tun) discover(link *link) { - tick := time.NewTicker(DiscoverTime) - defer tick.Stop() - - for { - select { - case <-tick.C: - // send a discovery message to all links - if err := link.Send(&transport.Message{ - Header: map[string]string{ - "Micro-Tunnel": "discover", - "Micro-Tunnel-Id": t.id, - "Micro-Tunnel-Token": t.token, - }, - }); err != nil { - log.Debugf("Tunnel failed to send discover to link %s: %v", link.id, err) - } - case <-link.closed: - return - case <-t.closed: - return - } - } -} - -// keepalive periodically sends keepalive messages to link -func (t *tun) keepalive(link *link) { - keepalive := time.NewTicker(KeepAliveTime) - defer keepalive.Stop() - - for { - select { - case <-t.closed: - return - case <-link.closed: - return - case <-keepalive.C: - // send keepalive message - log.Debugf("Tunnel sending keepalive to link: %v", link.Remote()) - if err := link.Send(&transport.Message{ - Header: map[string]string{ - "Micro-Tunnel": "keepalive", - "Micro-Tunnel-Id": t.id, - "Micro-Tunnel-Token": t.token, - }, - }); err != nil { - log.Debugf("Error sending keepalive to link %v: %v", link.Remote(), err) - t.delLink(link.Remote()) - return - } - } - } +func (t *tun) sendMsg(method string, link *link) error { + return link.Send(&transport.Message{ + Header: map[string]string{ + "Micro-Tunnel": method, + "Micro-Tunnel-Id": t.id, + }, + }) } // setupLink connects to node and returns link if successful // It returns error if the link failed to be established func (t *tun) setupLink(node string) (*link, error) { log.Debugf("Tunnel setting up link: %s", node) + c, err := t.options.Transport.Dial(node) if err != nil { log.Debugf("Tunnel failed to connect to %s: %v", node, err) return nil, err } - log.Debugf("Tunnel connected to %s", node) - // send the first connect message - if err := c.Send(&transport.Message{ - Header: map[string]string{ - "Micro-Tunnel": "connect", - "Micro-Tunnel-Id": t.id, - "Micro-Tunnel-Token": t.token, - }, - }); err != nil { - return nil, err - } + log.Debugf("Tunnel connected to %s", node) // create a new link link := newLink(c) // set link id to remote side link.id = c.Remote() + + // send the first connect message + if err := t.sendMsg("connect", link); err != nil { + link.Close() + return nil, err + } + // we made the outbound connection // and sent the connect message link.connected = true @@ -768,15 +800,42 @@ func (t *tun) setupLink(node string) (*link, error) { // process incoming messages go t.listen(link) - // start keepalive monitor - go t.keepalive(link) - - // discover things on the remote side - go t.discover(link) + // manage keepalives and discovery messages + go t.manageLink(link) return link, nil } +func (t *tun) setupLinks() { + var wg sync.WaitGroup + + for _, node := range t.options.Nodes { + wg.Add(1) + + go func(node string) { + defer wg.Done() + + // we're not trying to fix existing links + if _, ok := t.links[node]; ok { + return + } + + // create new link + link, err := t.setupLink(node) + if err != nil { + log.Debugf("Tunnel failed to setup node link to %s: %v", node, err) + return + } + + // save the link + t.links[node] = link + }(node) + } + + // wait for all threads to finish + wg.Wait() +} + // connect the tunnel to all the nodes and listen for incoming tunnel connections func (t *tun) connect() error { l, err := t.options.Transport.Listen(t.options.Address) @@ -795,11 +854,8 @@ func (t *tun) connect() error { // create a new link link := newLink(sock) - // start keepalive monitor - go t.keepalive(link) - - // discover things on the remote side - go t.discover(link) + // manage the link + go t.manageLink(link) // listen for inbound messages. // only save the link once connected. @@ -816,30 +872,6 @@ func (t *tun) connect() error { } }() - for _, node := range t.options.Nodes { - // skip zero length nodes - if len(node) == 0 { - continue - } - - // connect to node and return link - link, err := t.setupLink(node) - if err != nil { - log.Debugf("Tunnel failed to establish node link to %s: %v", node, err) - continue - } - - // save the link - t.links[node] = link - } - - // process outbound messages to be sent - // process sends to all links - go t.process() - - // monitor links - go t.monitor() - return nil } @@ -850,10 +882,13 @@ func (t *tun) Connect() error { // already connected if t.connected { + // do it immediately + t.setupLinks() + // setup links return nil } - // send the connect message + // connect the tunnel: start the listener if err := t.connect(); err != nil { return err } @@ -863,6 +898,16 @@ func (t *tun) Connect() error { // create new close channel t.closed = make(chan bool) + // process outbound messages to be sent + // process sends to all links + go t.process() + + // call setup before managing them + t.setupLinks() + + // manage the links + go t.manage() + return nil } @@ -877,9 +922,8 @@ func (t *tun) close() error { for node, link := range t.links { link.Send(&transport.Message{ Header: map[string]string{ - "Micro-Tunnel": "close", - "Micro-Tunnel-Id": t.id, - "Micro-Tunnel-Token": t.token, + "Micro-Tunnel": "close", + "Micro-Tunnel-Id": t.id, }, }) link.Close() @@ -891,6 +935,53 @@ func (t *tun) close() error { return t.listener.Close() } +// pickLink will pick the best link based on connectivity, delay, rate and length +func (t *tun) pickLink(links []*link) *link { + var metric float64 + var chosen *link + + // find the best link + for i, link := range links { + // don't use disconnected or errored links + if link.State() != "connected" { + continue + } + + // get the link state info + d := float64(link.Delay()) + l := float64(link.Length()) + r := link.Rate() + + // metric = delay x length x rate + m := d * l * r + + // first link so just and go + if i == 0 { + metric = m + chosen = link + continue + } + + // we found a better metric + if m < metric { + metric = m + chosen = link + } + } + + // if there's no link we're just going to mess around + if chosen == nil { + i := rand.Intn(len(links)) + return links[i] + } + + // we chose the link with; + // the lowest delay e.g least messages queued + // the lowest rate e.g the least messages flowing + // the lowest length e.g the smallest roundtrip time + return chosen +} + func (t *tun) Address() string { t.RLock() defer t.RUnlock() @@ -919,14 +1010,12 @@ func (t *tun) Close() error { default: close(t.closed) t.connected = false - - // send a close message - // we don't close the link - // just the tunnel - return t.close() } - return nil + // send a close message + // we don't close the link + // just the tunnel + return t.close() } // Dial an address @@ -953,138 +1042,142 @@ func (t *tun) Dial(channel string, opts ...DialOption) (Session, error) { } // set the multicast option - c.multicast = options.Multicast + c.mode = options.Mode // set the dial timeout - c.timeout = options.Timeout + c.dialTimeout = options.Timeout + // set read timeout set to never + c.readTimeout = time.Duration(-1) - now := time.Now() + var links []*link + // did we measure the rtt + var measured bool - after := func() time.Duration { - d := time.Since(now) - // dial timeout minus time since - wait := options.Timeout - d - if wait < time.Duration(0) { - return time.Duration(0) - } - return wait - } - - var links []string + t.RLock() // non multicast so we need to find the link - t.RLock() for _, link := range t.links { // use the link specified it its available if id := options.Link; len(id) > 0 && link.id != id { continue } - link.RLock() - _, ok := link.channels[channel] - link.RUnlock() + // get the channel + lastMapped := link.getChannel(channel) // we have at least one channel mapping - if ok { + if !lastMapped.IsZero() { + links = append(links, link) c.discovered = true - links = append(links, link.id) } } + t.RUnlock() - // link not found + // link not found and one was specified so error out if len(links) == 0 && len(options.Link) > 0 { + // delete session and return error + t.delSession(c.channel, c.session) + log.Debugf("Tunnel deleting session %s %s: %v", c.session, c.channel, ErrLinkNotFound) return nil, ErrLinkNotFound } // discovered so set the link if not multicast - // TODO: pick the link efficiently based - // on link status and saturation. - if c.discovered && !c.multicast { + if c.discovered && c.mode == Unicast { + // pickLink will pick the best link + link := t.pickLink(links) // set the link - i := rand.Intn(len(links)) - c.link = links[i] + c.link = link.id } - // shit fuck + // if its not already discovered we need to attempt to do so if !c.discovered { - // create a new discovery message for this channel - msg := c.newMessage("discover") - msg.broadcast = true - msg.outbound = true - msg.link = "" + // piggy back roundtrip + nowRTT := time.Now() - // send the discovery message - t.send <- msg - - select { - case <-time.After(after()): - return nil, ErrDialTimeout - case err := <-c.errChan: - if err != nil { - return nil, err - } - } - - var err error - - // set a dialTimeout - dialTimeout := after() - - // set a shorter delay for multicast - if c.multicast { - // shorten this - dialTimeout = time.Millisecond * 500 - } - - // wait for announce - select { - case msg := <-c.recv: - if msg.typ != "announce" { - err = ErrDiscoverChan - } - case <-time.After(dialTimeout): - err = ErrDialTimeout - } - - // if its multicast just go ahead because this is best effort - if c.multicast { - c.discovered = true - c.accepted = true - return c, nil - } - - // otherwise return an error + // attempt to discover the link + err := c.Discover() if err != nil { + t.delSession(c.channel, c.session) + log.Debugf("Tunnel deleting session %s %s: %v", c.session, c.channel, err) return nil, err } - // set discovered to true - c.discovered = true + // set roundtrip + d := time.Since(nowRTT) + + // set the link time + t.RLock() + link, ok := t.links[c.link] + t.RUnlock() + + if ok { + // set the rountrip time + link.setRTT(d) + // set measured to true + measured = true + } } - // a unicast session so we call "open" and wait for an "accept" + // return early if its not unicast + // we will not call "open" for multicast + if c.mode != Unicast { + return c, nil + } + + // Note: we go no further for multicast or broadcast. + // This is a unicast session so we call "open" and wait + // for an "accept" + + // reset now in case we use it + now := time.Now() // try to open the session - err := c.Open() - if err != nil { + if err := c.Open(); err != nil { // delete the session t.delSession(c.channel, c.session) + log.Debugf("Tunnel deleting session %s %s: %v", c.session, c.channel, err) return nil, err } + // set time take to open + d := time.Since(now) + + // if we haven't measured the roundtrip do it now + if !measured { + // set the link time + t.RLock() + link, ok := t.links[c.link] + t.RUnlock() + + if ok { + // set the rountrip time + link.setRTT(d) + } + } + return c, nil } // Accept a connection on the address -func (t *tun) Listen(channel string) (Listener, error) { +func (t *tun) Listen(channel string, opts ...ListenOption) (Listener, error) { log.Debugf("Tunnel listening on %s", channel) + options := ListenOptions{ + // Read timeout defaults to never + Timeout: time.Duration(-1), + } + + for _, o := range opts { + o(&options) + } + // create a new session by hashing the address c, ok := t.newSession(channel, "listener") if !ok { return nil, errors.New("already listening on " + channel) } + // delete function removes the session when closed delFunc := func() { t.delSession(channel, "listener") } @@ -1093,9 +1186,15 @@ func (t *tun) Listen(channel string) (Listener, error) { c.remote = "remote" // set local c.local = channel + // set mode + c.mode = options.Mode + // set the timeout + c.readTimeout = options.Timeout tl := &tunListener{ channel: channel, + // tunnel token + token: t.token, // the accept channel accept: make(chan *session, 128), // the channel to close @@ -1125,7 +1224,7 @@ func (t *tun) Links() []Link { t.RLock() defer t.RUnlock() - var links []Link + links := make([]Link, 0, len(t.links)) for _, link := range t.links { links = append(links, link) diff --git a/tunnel/link.go b/tunnel/link.go index f2395727..a072a047 100644 --- a/tunnel/link.go +++ b/tunnel/link.go @@ -1,19 +1,33 @@ package tunnel import ( + "bytes" + "errors" + "io" "sync" "time" "github.com/google/uuid" "github.com/micro/go-micro/transport" + "github.com/micro/go-micro/util/log" ) type link struct { transport.Socket + // transport to use for connections + transport transport.Transport + sync.RWMutex + // stops the link closed chan bool + // link state channel for testing link + state chan *packet + // send queue for sending packets + sendQueue chan *packet + // receive queue for receiving packets + recvQueue chan *packet // unique id of this link e.g uuid // which we define for ourselves id string @@ -31,32 +45,248 @@ type link struct { lastKeepAlive time.Time // channels keeps a mapping of channels and last seen channels map[string]time.Time - + // the weighted moving average roundtrip + length int64 + // weighted moving average of bits flowing + rate float64 // keep an error count on the link errCount int } +// packet send over link +type packet struct { + // message to send or received + message *transport.Message + + // status returned when sent + status chan error + + // receive related error + err error +} + +var ( + // the 4 byte 0 packet sent to determine the link state + linkRequest = []byte{0, 0, 0, 0} + // the 4 byte 1 filled packet sent to determine link state + linkResponse = []byte{1, 1, 1, 1} + + ErrLinkConnectTimeout = errors.New("link connect timeout") +) + func newLink(s transport.Socket) *link { l := &link{ Socket: s, id: uuid.New().String(), - channels: make(map[string]time.Time), - closed: make(chan bool), lastKeepAlive: time.Now(), + closed: make(chan bool), + channels: make(map[string]time.Time), + state: make(chan *packet, 64), + sendQueue: make(chan *packet, 128), + recvQueue: make(chan *packet, 128), } - go l.expiry() + + // process inbound/outbound packets + go l.process() + // manage the link state + go l.manage() + return l } -// watches the channel expiry -func (l *link) expiry() { - t := time.NewTicker(time.Minute) - defer t.Stop() +func (l *link) connect(addr string) error { + c, err := l.transport.Dial(addr) + if err != nil { + return err + } + + l.Lock() + l.Socket = c + l.Unlock() + + return nil +} + +func (l *link) accept(sock transport.Socket) error { + l.Lock() + l.Socket = sock + l.Unlock() + return nil +} + +func (l *link) setLoopback(v bool) { + l.Lock() + l.loopback = v + l.Unlock() +} + +// setRate sets the bits per second rate as a float64 +func (l *link) setRate(bits int64, delta time.Duration) { + // rate of send in bits per nanosecond + rate := float64(bits) / float64(delta.Nanoseconds()) + + // default the rate if its zero + if l.rate == 0 { + // rate per second + l.rate = rate * 1e9 + } else { + // set new rate per second + l.rate = 0.8*l.rate + 0.2*(rate*1e9) + } +} + +// setRTT sets a nanosecond based moving average roundtrip time for the link +func (l *link) setRTT(d time.Duration) { + l.Lock() + defer l.Unlock() + + if l.length <= 0 { + l.length = d.Nanoseconds() + return + } + + // https://fishi.devtail.io/weblog/2015/04/12/measuring-bandwidth-and-round-trip-time-tcp-connection-inside-application-layer/ + length := 0.8*float64(l.length) + 0.2*float64(d.Nanoseconds()) + // set new length + l.length = int64(length) +} + +func (l *link) delChannel(ch string) { + l.Lock() + delete(l.channels, ch) + l.Unlock() +} + +func (l *link) getChannel(ch string) time.Time { + l.RLock() + defer l.RUnlock() + return l.channels[ch] +} + +func (l *link) setChannel(channels ...string) { + l.Lock() + for _, ch := range channels { + l.channels[ch] = time.Now() + } + l.Unlock() +} + +// set the keepalive time +func (l *link) keepalive() { + l.Lock() + l.lastKeepAlive = time.Now() + l.Unlock() +} + +// process deals with the send queue +func (l *link) process() { + // receive messages + go func() { + for { + m := new(transport.Message) + err := l.recv(m) + if err != nil { + l.Lock() + l.errCount++ + l.Unlock() + } + + // process new received message + + pk := &packet{message: m, err: err} + + // this is our link state packet + if m.Header["Micro-Method"] == "link" { + // process link state message + select { + case l.state <- pk: + case <-l.closed: + return + default: + } + continue + } + + // process all messages as is + + select { + case l.recvQueue <- pk: + case <-l.closed: + return + } + } + }() + + // send messages for { select { + case pk := <-l.sendQueue: + // send the message + select { + case pk.status <- l.send(pk.message): + case <-l.closed: + return + } case <-l.closed: return + } + } +} + +// manage manages the link state including rtt packets and channel mapping expiry +func (l *link) manage() { + // tick over every minute to expire and fire rtt packets + t := time.NewTicker(time.Minute) + defer t.Stop() + + // get link id + linkId := l.Id() + + // used to send link state packets + send := func(b []byte) error { + return l.Send(&transport.Message{ + Header: map[string]string{ + "Micro-Method": "link", + "Micro-Link-Id": linkId, + }, Body: b, + }) + } + + // set time now + now := time.Now() + + // send the initial rtt request packet + send(linkRequest) + + for { + select { + // exit if closed + case <-l.closed: + return + // process link state rtt packets + case p := <-l.state: + if p.err != nil { + continue + } + // check the type of message + switch { + case bytes.Equal(p.message.Body, linkRequest): + log.Tracef("Link %s received link request", linkId) + + // send response + if err := send(linkResponse); err != nil { + l.Lock() + l.errCount++ + l.Unlock() + } + case bytes.Equal(p.message.Body, linkResponse): + // set round trip time + d := time.Since(now) + log.Tracef("Link %s received link response in %v", linkId, d) + // set the RTT + l.setRTT(d) + } case <-t.C: // drop any channel mappings older than 2 minutes var kill []string @@ -81,47 +311,166 @@ func (l *link) expiry() { delete(l.channels, ch) } l.Unlock() + + // fire off a link state rtt packet + now = time.Now() + send(linkRequest) } } } +func (l *link) send(m *transport.Message) error { + if m.Header == nil { + m.Header = make(map[string]string) + } + // send the message + return l.Socket.Send(m) +} + +// recv a message on the link +func (l *link) recv(m *transport.Message) error { + if m.Header == nil { + m.Header = make(map[string]string) + } + // receive the transport message + return l.Socket.Recv(m) +} + +// Delay is the current load on the link +func (l *link) Delay() int64 { + return int64(len(l.sendQueue) + len(l.recvQueue)) +} + +// Current transfer rate as bits per second (lower is better) +func (l *link) Rate() float64 { + l.RLock() + defer l.RUnlock() + return l.rate +} + +func (l *link) Loopback() bool { + l.RLock() + defer l.RUnlock() + return l.loopback +} + +// Length returns the roundtrip time as nanoseconds (lower is better). +// Returns 0 where no measurement has been taken. +func (l *link) Length() int64 { + l.RLock() + defer l.RUnlock() + return l.length +} + func (l *link) Id() string { l.RLock() defer l.RUnlock() - return l.id } func (l *link) Close() error { + l.Lock() + defer l.Unlock() + select { case <-l.closed: return nil default: + l.Socket.Close() close(l.closed) - return nil } return nil } +// Send sencs a message on the link func (l *link) Send(m *transport.Message) error { - err := l.Socket.Send(m) + // create a new packet to send over the link + p := &packet{ + message: m, + status: make(chan error, 1), + } + + // get time now + now := time.Now() + + // queue the message + select { + case l.sendQueue <- p: + // in the send queue + case <-l.closed: + return io.EOF + } + + // error to use + var err error + + // wait for response + select { + case <-l.closed: + return io.EOF + case err = <-p.status: + } l.Lock() defer l.Unlock() - // if theres no error reset the counter - if err == nil { - l.errCount = 0 + // there's an error increment the counter and bail + if err != nil { + l.errCount++ + return err } - // otherwise increment the counter - l.errCount++ + // reset the counter + l.errCount = 0 - return err + // calculate the data sent + dataSent := len(m.Body) + + // set header length + for k, v := range m.Header { + dataSent += (len(k) + len(v)) + } + + // calculate based on data + if dataSent > 0 { + // bit sent + bits := dataSent * 1024 + + // set the rate + l.setRate(int64(bits), time.Since(now)) + } + + return nil } -func (l *link) Status() string { +// Accept accepts a message on the socket +func (l *link) Recv(m *transport.Message) error { + select { + case <-l.closed: + // check if there's any messages left + select { + case pk := <-l.recvQueue: + // check the packet receive error + if pk.err != nil { + return pk.err + } + *m = *pk.message + default: + return io.EOF + } + case pk := <-l.recvQueue: + // check the packet receive error + if pk.err != nil { + return pk.err + } + *m = *pk.message + } + return nil +} + +// State can return connected, closed, error +func (l *link) State() string { select { case <-l.closed: return "closed" diff --git a/tunnel/listener.go b/tunnel/listener.go index ee394519..6dbec5c8 100644 --- a/tunnel/listener.go +++ b/tunnel/listener.go @@ -10,6 +10,8 @@ import ( type tunListener struct { // address of the listener channel string + // token is the tunnel token + token string // the accept channel accept chan *session // the channel to close @@ -22,7 +24,7 @@ type tunListener struct { delFunc func() } -// periodically announce self +// periodically announce self the channel being listened on func (t *tunListener) announce() { tick := time.NewTicker(time.Second * 30) defer tick.Stop() @@ -46,9 +48,12 @@ func (t *tunListener) process() { defer func() { // close the sessions - for _, conn := range conns { + for id, conn := range conns { conn.Close() + delete(conns, id) } + // unassign + conns = nil }() for { @@ -60,10 +65,26 @@ func (t *tunListener) process() { return // receive a new message case m := <-t.session.recv: + var sessionId string + var linkId string + + switch m.mode { + case Multicast: + sessionId = "multicast" + linkId = "multicast" + case Broadcast: + sessionId = "broadcast" + linkId = "broadcast" + default: + sessionId = m.session + linkId = m.link + } + // get a session - sess, ok := conns[m.session] - log.Debugf("Tunnel listener received channel %s session %s exists: %t", m.channel, m.session, ok) + sess, ok := conns[sessionId] + log.Tracef("Tunnel listener received channel %s session %s type %s exists: %t", m.channel, m.session, m.typ, ok) if !ok { + // we only process open and session types switch m.typ { case "open", "session": default: @@ -77,27 +98,29 @@ func (t *tunListener) process() { // the channel channel: m.channel, // the session id - session: m.session, + session: sessionId, + // tunnel token + token: t.token, // is loopback conn loopback: m.loopback, // the link the message was received on - link: m.link, - // set multicast - multicast: m.multicast, + link: linkId, + // set the connection mode + mode: m.mode, // close chan closed: make(chan bool), // recv called by the acceptor recv: make(chan *message, 128), // use the internal send buffer send: t.session.send, - // wait - wait: make(chan bool), // error channel errChan: make(chan error, 1), + // set the read timeout + readTimeout: t.session.readTimeout, } // save the session - conns[m.session] = sess + conns[sessionId] = sess select { case <-t.closed: @@ -109,17 +132,19 @@ func (t *tunListener) process() { // an existing session was found - // received a close message switch m.typ { case "close": + // received a close message select { + // check if the session is closed case <-sess.closed: // no op - delete(conns, m.session) + delete(conns, sessionId) default: + // only close if unicast session // close and delete session close(sess.closed) - delete(conns, m.session) + delete(conns, sessionId) } // continue @@ -134,9 +159,9 @@ func (t *tunListener) process() { // send this to the accept chan select { case <-sess.closed: - delete(conns, m.session) + delete(conns, sessionId) case sess.recv <- m: - log.Debugf("Tunnel listener sent to recv chan channel %s session %s", m.channel, m.session) + log.Tracef("Tunnel listener sent to recv chan channel %s session %s type %s", m.channel, sessionId, m.typ) } } } @@ -175,11 +200,14 @@ func (t *tunListener) Accept() (Session, error) { if !ok { return nil, io.EOF } + // return without accept + if c.mode != Unicast { + return c, nil + } // send back the accept if err := c.Accept(); err != nil { return nil, err } return c, nil } - return nil, nil } diff --git a/tunnel/options.go b/tunnel/options.go index 903f7fb7..145db19f 100644 --- a/tunnel/options.go +++ b/tunnel/options.go @@ -12,7 +12,7 @@ var ( // DefaultAddress is default tunnel bind address DefaultAddress = ":0" // The shared default token - DefaultToken = "micro" + DefaultToken = "go.micro.tunnel" ) type Option func(*Options) @@ -36,12 +36,21 @@ type DialOption func(*DialOptions) type DialOptions struct { // Link specifies the link to use Link string - // specify a multicast connection - Multicast bool + // specify mode of the session + Mode Mode // the dial timeout Timeout time.Duration } +type ListenOption func(*ListenOptions) + +type ListenOptions struct { + // specify mode of the session + Mode Mode + // The read timeout + Timeout time.Duration +} + // The tunnel id func Id(id string) Option { return func(o *Options) { @@ -77,22 +86,26 @@ func Transport(t transport.Transport) Option { } } -// DefaultOptions returns router default options -func DefaultOptions() Options { - return Options{ - Id: uuid.New().String(), - Address: DefaultAddress, - Token: DefaultToken, - Transport: quic.NewTransport(), +// Listen options +func ListenMode(m Mode) ListenOption { + return func(o *ListenOptions) { + o.Mode = m + } +} + +// Timeout for reads and writes on the listener session +func ListenTimeout(t time.Duration) ListenOption { + return func(o *ListenOptions) { + o.Timeout = t } } // Dial options // Dial multicast sets the multicast option to send only to those mapped -func DialMulticast() DialOption { +func DialMode(m Mode) DialOption { return func(o *DialOptions) { - o.Multicast = true + o.Mode = m } } @@ -110,3 +123,13 @@ func DialLink(id string) DialOption { o.Link = id } } + +// DefaultOptions returns router default options +func DefaultOptions() Options { + return Options{ + Id: uuid.New().String(), + Address: DefaultAddress, + Token: DefaultToken, + Transport: quic.NewTransport(), + } +} diff --git a/tunnel/session.go b/tunnel/session.go index e0eb8829..84153541 100644 --- a/tunnel/session.go +++ b/tunnel/session.go @@ -1,7 +1,7 @@ package tunnel import ( - "errors" + "encoding/hex" "io" "time" @@ -17,6 +17,8 @@ type session struct { channel string // the session id based on Micro.Tunnel-Session session string + // token is the session token + token string // closed closed chan bool // remote addr @@ -27,8 +29,6 @@ type session struct { send chan *message // recv chan recv chan *message - // wait until we have a connection - wait chan bool // if the discovery worked discovered bool // if the session was accepted @@ -37,12 +37,12 @@ type session struct { outbound bool // lookback marks the session as a loopback on the inbound loopback bool - // if the session is multicast - multicast bool - // if the session is broadcast - broadcast bool - // the timeout - timeout time.Duration + // mode of the connection + mode Mode + // the dial timeout + dialTimeout time.Duration + // the read timeout + readTimeout time.Duration // the link on which this message was received link string // the error response @@ -63,10 +63,8 @@ type message struct { outbound bool // loopback marks the message intended for loopback loopback bool - // whether to send as multicast - multicast bool - // broadcast sets the broadcast type - broadcast bool + // mode of the connection + mode Mode // the link to send the message on link string // transport data @@ -98,27 +96,29 @@ func (s *session) Channel() string { // newMessage creates a new message based on the session func (s *session) newMessage(typ string) *message { return &message{ - typ: typ, - tunnel: s.tunnel, - channel: s.channel, - session: s.session, - outbound: s.outbound, - loopback: s.loopback, - multicast: s.multicast, - link: s.link, - errChan: s.errChan, + typ: typ, + tunnel: s.tunnel, + channel: s.channel, + session: s.session, + outbound: s.outbound, + loopback: s.loopback, + mode: s.mode, + link: s.link, + errChan: s.errChan, } } -// Open will fire the open message for the session. This is called by the dialler. -func (s *session) Open() error { - // create a new message - msg := s.newMessage("open") +func (s *session) sendMsg(msg *message) error { + select { + case <-s.closed: + return io.EOF + case s.send <- msg: + return nil + } +} - // send open message - s.send <- msg - - // wait for an error response for send +func (s *session) wait(msg *message) error { + // wait for an error response select { case err := <-msg.errChan: if err != nil { @@ -128,29 +128,151 @@ func (s *session) Open() error { return io.EOF } - // we don't wait on multicast - if s.multicast { + return nil +} + +// waitFor waits for the message type required until the timeout specified +func (s *session) waitFor(msgType string, timeout time.Duration) (*message, error) { + now := time.Now() + + after := func(timeout time.Duration) <-chan time.Time { + if timeout < time.Duration(0) { + return nil + } + + // get the delta + d := time.Since(now) + + // dial timeout minus time since + wait := timeout - d + + if wait < time.Duration(0) { + wait = time.Duration(0) + } + + return time.After(wait) + } + + // wait for the message type + for { + select { + case msg := <-s.recv: + // there may be no message type + if len(msgType) == 0 { + return msg, nil + } + + // ignore what we don't want + if msg.typ != msgType { + log.Debugf("Tunnel received non %s message in waiting for %s", msg.typ, msgType) + continue + } + + // got the message + return msg, nil + case <-after(timeout): + return nil, ErrReadTimeout + case <-s.closed: + return nil, io.EOF + } + } +} + +// Discover attempts to discover the link for a specific channel. +// This is only used by the tunnel.Dial when first connecting. +func (s *session) Discover() error { + // create a new discovery message for this channel + msg := s.newMessage("discover") + // broadcast the message to all links + msg.mode = Broadcast + // its an outbound connection since we're dialling + msg.outbound = true + // don't set the link since we don't know where it is + msg.link = "" + + // if multicast then set that as session + if s.mode == Multicast { + msg.session = "multicast" + } + + // send discover message + if err := s.sendMsg(msg); err != nil { + return err + } + + // set time now + now := time.Now() + + // after strips down the dial timeout + after := func() time.Duration { + d := time.Since(now) + // dial timeout minus time since + wait := s.dialTimeout - d + // make sure its always > 0 + if wait < time.Duration(0) { + return time.Duration(0) + } + return wait + } + + // the discover message is sent out, now + // wait to hear back about the sent message + select { + case <-time.After(after()): + return ErrDialTimeout + case err := <-s.errChan: + if err != nil { + return err + } + } + + // bail early if its not unicast + // we don't need to wait for the announce + if s.mode != Unicast { + s.discovered = true s.accepted = true return nil } - // now wait for the accept - select { - case msg = <-s.recv: - if msg.typ != "accept" { - log.Debugf("Received non accept message in Open %s", msg.typ) - return errors.New("failed to connect") - } - // set to accepted - s.accepted = true - // set link - s.link = msg.link - case <-time.After(s.timeout): - return ErrDialTimeout - case <-s.closed: - return io.EOF + // wait for announce + _, err := s.waitFor("announce", after()) + if err != nil { + return err } + // set discovered + s.discovered = true + + return nil +} + +// Open will fire the open message for the session. This is called by the dialler. +// This is to indicate that we want to create a new session. +func (s *session) Open() error { + // create a new message + msg := s.newMessage("open") + + // send open message + if err := s.sendMsg(msg); err != nil { + return err + } + + // wait for an error response for send + if err := s.wait(msg); err != nil { + return err + } + + // now wait for the accept message to be returned + msg, err := s.waitFor("accept", s.dialTimeout) + if err != nil { + return err + } + + // set to accepted + s.accepted = true + // set link + s.link = msg.link + return nil } @@ -159,61 +281,54 @@ func (s *session) Accept() error { msg := s.newMessage("accept") // send the accept message - select { - case <-s.closed: - return io.EOF - case s.send <- msg: - return nil + if err := s.sendMsg(msg); err != nil { + return err } // wait for send response - select { - case err := <-s.errChan: - if err != nil { - return err - } - case <-s.closed: - return io.EOF - } - - return nil + return s.wait(msg) } -// Announce sends an announcement to notify that this session exists. This is primarily used by the listener. +// Announce sends an announcement to notify that this session exists. +// This is primarily used by the listener. func (s *session) Announce() error { msg := s.newMessage("announce") // we don't need an error back msg.errChan = nil // announce to all - msg.broadcast = true + msg.mode = Broadcast // we don't need the link msg.link = "" - select { - case s.send <- msg: - return nil - case <-s.closed: - return io.EOF - } + // send announce message + return s.sendMsg(msg) } // Send is used to send a message func (s *session) Send(m *transport.Message) error { - select { - case <-s.closed: - return io.EOF - default: - // no op + // encrypt the transport message payload + body, err := Encrypt(m.Body, s.token+s.channel+s.session) + if err != nil { + log.Debugf("failed to encrypt message body: %v", err) + return err } // make copy data := &transport.Message{ Header: make(map[string]string), - Body: m.Body, + Body: body, } + // encrypt all the headers for k, v := range m.Header { - data.Header[k] = v + // encrypt the transport message payload + val, err := Encrypt([]byte(v), s.token+s.channel+s.session) + if err != nil { + log.Debugf("failed to encrypt message header %s: %v", k, err) + return err + } + // hex encode the encrypted header value + data.Header[k] = hex.EncodeToString(val) } // create a new message @@ -222,35 +337,29 @@ func (s *session) Send(m *transport.Message) error { msg.data = data // if multicast don't set the link - if s.multicast { + if s.mode != Unicast { msg.link = "" } - log.Debugf("Appending %+v to send backlog", msg) - // send the actual message - s.send <- msg + log.Tracef("Appending %+v to send backlog", msg) - // wait for an error response - select { - case err := <-msg.errChan: + // send the actual message + if err := s.sendMsg(msg); err != nil { return err - case <-s.closed: - return io.EOF } - return nil + // wait for an error response + return s.wait(msg) } // Recv is used to receive a message func (s *session) Recv(m *transport.Message) error { - select { - case <-s.closed: - return errors.New("session is closed") - default: - // no op + var msg *message + + msg, err := s.waitFor("", s.readTimeout) + if err != nil { + return err } - // recv from backlog - msg := <-s.recv // check the error if one exists select { @@ -259,7 +368,44 @@ func (s *session) Recv(m *transport.Message) error { default: } - log.Debugf("Received %+v from recv backlog", msg) + //log.Tracef("Received %+v from recv backlog", msg) + log.Tracef("Received %+v from recv backlog", msg) + + // decrypt the received payload using the token + // we have to used msg.session because multicast has a shared + // session id of "multicast" in this session struct on + // the listener side + body, err := Decrypt(msg.data.Body, s.token+s.channel+msg.session) + if err != nil { + log.Debugf("failed to decrypt message body: %v", err) + return err + } + msg.data.Body = body + + // encrypt all the headers + for k, v := range msg.data.Header { + // hex decode the header values + h, err := hex.DecodeString(v) + if err != nil { + log.Debugf("failed to decode message header %s: %v", k, err) + return err + } + // encrypt the transport message payload + val, err := Decrypt([]byte(h), s.token+s.channel+msg.session) + if err != nil { + log.Debugf("failed to decrypt message header %s: %v", k, err) + return err + } + // hex encode the encrypted header value + msg.data.Header[k] = string(val) + } + + // set the link + // TODO: decruft, this is only for multicast + // since the session is now a single session + // likely provide as part of message.Link() + msg.data.Header["Micro-Link"] = msg.link + // set message *m = *msg.data // return nil @@ -274,6 +420,11 @@ func (s *session) Close() error { default: close(s.closed) + // don't send close on multicast or broadcast + if s.mode != Unicast { + return nil + } + // append to backlog msg := s.newMessage("close") // no error response on close @@ -282,7 +433,7 @@ func (s *session) Close() error { // send the close message select { case s.send <- msg: - default: + case <-time.After(time.Millisecond * 10): } } diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 312a6681..15ff6078 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -8,6 +8,15 @@ import ( "github.com/micro/go-micro/transport" ) +const ( + // send over one link + Unicast Mode = iota + // send to all channel listeners + Multicast + // send to all links + Broadcast +) + var ( // DefaultDialTimeout is the dial timeout if none is specified DefaultDialTimeout = time.Second * 5 @@ -17,36 +26,50 @@ var ( ErrDiscoverChan = errors.New("failed to discover channel") // ErrLinkNotFound is returned when a link is specified at dial time and does not exist ErrLinkNotFound = errors.New("link not found") + // ErrReadTimeout is a timeout on session.Recv + ErrReadTimeout = errors.New("read timeout") ) +// Mode of the session +type Mode uint8 + // Tunnel creates a gre tunnel on top of the go-micro/transport. // It establishes multiple streams using the Micro-Tunnel-Channel header // and Micro-Tunnel-Session header. The tunnel id is a hash of // the address being requested. type Tunnel interface { + // Init initializes tunnel with options Init(opts ...Option) error - // Address the tunnel is listening on + // Address returns the address the tunnel is listening on Address() string // Connect connects the tunnel Connect() error // Close closes the tunnel Close() error - // All the links the tunnel is connected to + // Links returns all the links the tunnel is connected to Links() []Link - // Connect to a channel + // Dial allows a client to connect to a channel Dial(channel string, opts ...DialOption) (Session, error) - // Accept connections on a channel - Listen(channel string) (Listener, error) - // Name of the tunnel implementation + // Listen allows to accept connections on a channel + Listen(channel string, opts ...ListenOption) (Listener, error) + // String returns the name of the tunnel implementation String() string } // Link represents internal links to the tunnel type Link interface { - // The id of the link + // Id returns the link unique Id Id() string - // Status of the link e.g connected/closed - Status() string + // Delay is the current load on the link (lower is better) + Delay() int64 + // Length returns the roundtrip time as nanoseconds (lower is better) + Length() int64 + // Current transfer rate as bits per second (lower is better) + Rate() float64 + // Is this a loopback link + Loopback() bool + // State of the link: connected/closed/error + State() string // honours transport socket transport.Socket } diff --git a/tunnel/tunnel_test.go b/tunnel/tunnel_test.go index fc76421b..9633e53d 100644 --- a/tunnel/tunnel_test.go +++ b/tunnel/tunnel_test.go @@ -8,6 +8,90 @@ import ( "github.com/micro/go-micro/transport" ) +func testBrokenTunAccept(t *testing.T, tun Tunnel, wait chan bool, wg *sync.WaitGroup) { + defer wg.Done() + + // listen on some virtual address + tl, err := tun.Listen("test-tunnel") + if err != nil { + t.Fatal(err) + } + + // receiver ready; notify sender + wait <- true + + // accept a connection + c, err := tl.Accept() + if err != nil { + t.Fatal(err) + } + + // accept the message and close the tunnel + // we do this to simulate loss of network connection + m := new(transport.Message) + if err := c.Recv(m); err != nil { + t.Fatal(err) + } + + // close all the links + for _, link := range tun.Links() { + link.Close() + } + + // receiver ready; notify sender + wait <- true + + // accept the message + m = new(transport.Message) + if err := c.Recv(m); err != nil { + t.Fatal(err) + } + + // notify the sender we have received + wait <- true +} + +func testBrokenTunSend(t *testing.T, tun Tunnel, wait chan bool, wg *sync.WaitGroup, reconnect time.Duration) { + defer wg.Done() + + // wait for the listener to get ready + <-wait + + // dial a new session + c, err := tun.Dial("test-tunnel") + if err != nil { + t.Fatal(err) + } + defer c.Close() + + m := transport.Message{ + Header: map[string]string{ + "test": "send", + }, + } + + // send the message + if err := c.Send(&m); err != nil { + t.Fatal(err) + } + + // wait for the listener to get ready + <-wait + + // give it time to reconnect + time.Sleep(reconnect) + + // send the message + if err := c.Send(&m); err != nil { + t.Fatal(err) + } + + // wait for the listener to receive the message + // c.Send merely enqueues the message to the link send queue and returns + // in order to verify it was received we wait for the listener to tell us + <-wait +} + // testAccept will accept connections on the transport, create a new link and tunnel on top func testAccept(t *testing.T, tun Tunnel, wait chan bool, wg *sync.WaitGroup) { defer wg.Done() @@ -163,91 +247,7 @@ func TestLoopbackTunnel(t *testing.T) { wg.Wait() } -func testBrokenTunAccept(t *testing.T, tun Tunnel, wait chan bool, wg *sync.WaitGroup) { - defer wg.Done() - - // listen on some virtual address - tl, err := tun.Listen("test-tunnel") - if err != nil { - t.Fatal(err) - } - - // receiver ready; notify sender - wait <- true - - // accept a connection - c, err := tl.Accept() - if err != nil { - t.Fatal(err) - } - - // accept the message and close the tunnel - // we do this to simulate loss of network connection - m := new(transport.Message) - if err := c.Recv(m); err != nil { - t.Fatal(err) - } - - // close all the links - for _, link := range tun.Links() { - link.Close() - } - - // receiver ready; notify sender - wait <- true - - // accept the message - m = new(transport.Message) - if err := c.Recv(m); err != nil { - t.Fatal(err) - } - - // notify sender we have received the message - <-wait -} - -func testBrokenTunSend(t *testing.T, tun Tunnel, wait chan bool, wg *sync.WaitGroup) { - defer wg.Done() - - // wait for the listener to get ready - <-wait - - // dial a new session - c, err := tun.Dial("test-tunnel") - if err != nil { - t.Fatal(err) - } - defer c.Close() - - m := transport.Message{ - Header: map[string]string{ - "test": "send", - }, - } - - // send the message - if err := c.Send(&m); err != nil { - t.Fatal(err) - } - - // wait for the listener to get ready - <-wait - - // give it time to reconnect - time.Sleep(2 * ReconnectTime) - - // send the message - if err := c.Send(&m); err != nil { - t.Fatal(err) - } - - // wait for the listener to receive the message - // c.Send merely enqueues the message to the link send queue and returns - // in order to verify it was received we wait for the listener to tell us - wait <- true -} - -func TestReconnectTunnel(t *testing.T) { +func TestTunnelRTTRate(t *testing.T) { // create a new tunnel client tunA := NewTunnel( Address("127.0.0.1:9096"), @@ -259,17 +259,67 @@ func TestReconnectTunnel(t *testing.T) { Address("127.0.0.1:9097"), ) - // start tunnel + // start tunB err := tunB.Connect() if err != nil { t.Fatal(err) } defer tunB.Close() + // start tunA + err = tunA.Connect() + if err != nil { + t.Fatal(err) + } + defer tunA.Close() + + wait := make(chan bool) + + var wg sync.WaitGroup + + wg.Add(1) + // start the listener + go testAccept(t, tunB, wait, &wg) + + wg.Add(1) + // start the client + go testSend(t, tunA, wait, &wg) + + // wait until done + wg.Wait() + + for _, link := range tunA.Links() { + t.Logf("Link %s length %v rate %v", link.Id(), link.Length(), link.Rate()) + } + + for _, link := range tunB.Links() { + t.Logf("Link %s length %v rate %v", link.Id(), link.Length(), link.Rate()) + } +} + +func TestReconnectTunnel(t *testing.T) { // we manually override the tunnel.ReconnectTime value here // this is so that we make the reconnects faster than the default 5s ReconnectTime = 200 * time.Millisecond + // create a new tunnel client + tunA := NewTunnel( + Address("127.0.0.1:9098"), + Nodes("127.0.0.1:9099"), + ) + + // create a new tunnel server + tunB := NewTunnel( + Address("127.0.0.1:9099"), + ) + + // start tunnel + err := tunB.Connect() + if err != nil { + t.Fatal(err) + } + defer tunB.Close() + // start tunnel err = tunA.Connect() if err != nil { @@ -287,7 +337,7 @@ func TestReconnectTunnel(t *testing.T) { wg.Add(1) // start tunnel sender - go testBrokenTunSend(t, tunA, wait, &wg) + go testBrokenTunSend(t, tunA, wait, &wg, ReconnectTime*5) // wait until done wg.Wait() diff --git a/util/addr/addr.go b/util/addr/addr.go index 911dacb0..eb0a5972 100644 --- a/util/addr/addr.go +++ b/util/addr/addr.go @@ -39,6 +39,7 @@ func Extract(addr string) (string, error) { return "", fmt.Errorf("Failed to get interfaces! Err: %v", err) } + //nolint:prealloc var addrs []net.Addr var loAddrs []net.Addr for _, iface := range ifaces { diff --git a/util/log/log.go b/util/log/log.go index c8ae4acc..52184c5e 100644 --- a/util/log/log.go +++ b/util/log/log.go @@ -2,6 +2,7 @@ package log import ( + "fmt" "os" "github.com/go-log/log" @@ -13,8 +14,9 @@ type Level int const ( LevelFatal Level = iota - LevelInfo LevelError + LevelInfo + LevelWarn LevelDebug LevelTrace ) @@ -25,6 +27,9 @@ var ( // default log level is info level = LevelInfo + + // prefix for all messages + prefix string ) func init() { @@ -33,6 +38,8 @@ func init() { level = LevelTrace case "debug": level = LevelDebug + case "warn": + level = LevelWarn case "info": level = LevelInfo case "error": @@ -44,11 +51,18 @@ func init() { // Log makes use of github.com/go-log/log.Log func Log(v ...interface{}) { + if len(prefix) > 0 { + logger.Log(append([]interface{}{prefix, " "}, v...)...) + return + } logger.Log(v...) } // Logf makes use of github.com/go-log/log.Logf func Logf(format string, v ...interface{}) { + if len(prefix) > 0 { + format = prefix + " " + format + } logger.Logf(format, v...) } @@ -88,6 +102,16 @@ func Debugf(format string, v ...interface{}) { WithLevelf(LevelDebug, format, v...) } +// Warn provides warn level logging +func Warn(v ...interface{}) { + WithLevel(LevelWarn, v...) +} + +// Warnf provides warn level logging +func Warnf(format string, v ...interface{}) { + WithLevelf(LevelWarn, format, v...) +} + // Info provides info level logging func Info(v ...interface{}) { WithLevel(LevelInfo, v...) @@ -139,3 +163,13 @@ func SetLevel(l Level) { func GetLevel() Level { return level } + +// Set a prefix for the logger +func SetPrefix(p string) { + prefix = p +} + +// Set service name +func Name(name string) { + prefix = fmt.Sprintf("[%s]", name) +} diff --git a/util/mux/mux.go b/util/mux/mux.go index 4741bd18..e10368ec 100644 --- a/util/mux/mux.go +++ b/util/mux/mux.go @@ -5,7 +5,7 @@ import ( "context" "sync" - "github.com/micro/go-micro/debug/handler" + "github.com/micro/go-micro/debug/service/handler" "github.com/micro/go-micro/proxy" "github.com/micro/go-micro/server" ) @@ -22,6 +22,13 @@ var ( once sync.Once ) +func (s *Server) ProcessMessage(ctx context.Context, msg server.Message) error { + if msg.Topic() == s.Name { + return server.DefaultRouter.ProcessMessage(ctx, msg) + } + return s.Proxy.ProcessMessage(ctx, msg) +} + func (s *Server) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error { if req.Service() == s.Name { return server.DefaultRouter.ServeRequest(ctx, req, rsp) diff --git a/util/socket/pool.go b/util/socket/pool.go new file mode 100644 index 00000000..9439f131 --- /dev/null +++ b/util/socket/pool.go @@ -0,0 +1,56 @@ +package socket + +import ( + "sync" +) + +type Pool struct { + sync.RWMutex + pool map[string]*Socket +} + +func (p *Pool) Get(id string) (*Socket, bool) { + // attempt to get existing socket + p.RLock() + socket, ok := p.pool[id] + if ok { + p.RUnlock() + return socket, ok + } + p.RUnlock() + + // create new socket + socket = New(id) + // save socket + p.Lock() + p.pool[id] = socket + p.Unlock() + // return socket + return socket, false +} + +func (p *Pool) Release(s *Socket) { + p.Lock() + defer p.Unlock() + + // close the socket + s.Close() + delete(p.pool, s.id) +} + +// Close the pool and delete all the sockets +func (p *Pool) Close() { + p.Lock() + defer p.Unlock() + for id, sock := range p.pool { + sock.Close() + delete(p.pool, id) + } +} + +// NewPool returns a new socket pool +func NewPool() *Pool { + return &Pool{ + pool: make(map[string]*Socket), + } +} diff --git a/util/socket/socket.go b/util/socket/socket.go index 29ba5006..e68df694 100644 --- a/util/socket/socket.go +++ b/util/socket/socket.go @@ -9,6 +9,7 @@ import ( // Socket is our pseudo socket for transport.Socket type Socket struct { + id string // closed closed chan bool // remote addr @@ -37,7 +38,6 @@ func (s *Socket) Accept(m *transport.Message) error { case <-s.closed: return io.EOF } - return nil } // Process takes the next message off the send queue created by a call to Send @@ -119,8 +119,9 @@ func (s *Socket) Close() error { // New returns a new pseudo socket which can be used in the place of a transport socket. // Messages are sent to the socket via Accept and receives from the socket via Process. // SetLocal/SetRemote should be called before using the socket. -func New() *Socket { +func New(id string) *Socket { return &Socket{ + id: id, closed: make(chan bool), local: "local", remote: "remote", diff --git a/common_test.go b/util/test/test.go similarity index 86% rename from common_test.go rename to util/test/test.go index d4fd4bfb..100c8e79 100644 --- a/common_test.go +++ b/util/test/test.go @@ -1,13 +1,13 @@ -package micro +package test import ( "github.com/micro/go-micro/registry" ) var ( - // mock data - testData = map[string][]*registry.Service{ - "foo": []*registry.Service{ + // mock registry data + Data = map[string][]*registry.Service{ + "foo": { { Name: "foo", Version: "1.0.0", diff --git a/wrapper.go b/util/wrapper/wrapper.go similarity index 78% rename from wrapper.go rename to util/wrapper/wrapper.go index e4977397..cf198435 100644 --- a/wrapper.go +++ b/util/wrapper/wrapper.go @@ -1,4 +1,4 @@ -package micro +package wrapper import ( "context" @@ -12,6 +12,10 @@ type clientWrapper struct { headers metadata.Metadata } +var ( + HeaderPrefix = "Micro-" +) + func (c *clientWrapper) setHeaders(ctx context.Context) context.Context { // copy metadata mda, _ := metadata.FromContext(ctx) @@ -41,3 +45,13 @@ func (c *clientWrapper) Publish(ctx context.Context, p client.Message, opts ...c ctx = c.setHeaders(ctx) return c.Client.Publish(ctx, p, opts...) } + +// FromService wraps a client to inject From-Service header into metadata +func FromService(name string, c client.Client) client.Client { + return &clientWrapper{ + c, + metadata.Metadata{ + HeaderPrefix + "From-Service": name, + }, + } +} diff --git a/wrapper_test.go b/util/wrapper/wrapper_test.go similarity index 98% rename from wrapper_test.go rename to util/wrapper/wrapper_test.go index 80abdc46..de777cee 100644 --- a/wrapper_test.go +++ b/util/wrapper/wrapper_test.go @@ -1,4 +1,4 @@ -package micro +package wrapper import ( "context" diff --git a/web/options.go b/web/options.go index 1c9bd6ee..9a31f6d0 100644 --- a/web/options.go +++ b/web/options.go @@ -72,6 +72,16 @@ func Name(n string) Option { } } +// Icon specifies an icon url to load in the UI +func Icon(ico string) Option { + return func(o *Options) { + if o.Metadata == nil { + o.Metadata = make(map[string]string) + } + o.Metadata["icon"] = ico + } +} + // Unique server id func Id(id string) Option { return func(o *Options) { diff --git a/web/service.go b/web/service.go index 65359a2b..49850d9e 100644 --- a/web/service.go +++ b/web/service.go @@ -82,7 +82,7 @@ func (s *service) genSrv() *registry.Service { return ®istry.Service{ Name: s.opts.Name, Version: s.opts.Version, - Nodes: []*registry.Node{®istry.Node{ + Nodes: []*registry.Node{{ Id: s.opts.Id, Address: fmt.Sprintf("%s:%d", addr, port), Metadata: s.opts.Metadata, @@ -118,6 +118,11 @@ func (s *service) register() error { if s.opts.Registry != nil { r = s.opts.Registry } + + // service node need modify, node address maybe changed + srv := s.genSrv() + srv.Endpoints = s.srv.Endpoints + s.srv = srv return r.Register(s.srv, registry.RegisterTTL(s.opts.RegisterTTL)) } @@ -211,7 +216,7 @@ func (s *service) start() error { ch <- l.Close() }() - log.Logf("Listening on %v\n", l.Addr().String()) + log.Logf("Listening on %v", l.Addr().String()) return nil } @@ -371,12 +376,12 @@ func (s *service) Run() error { go s.run(ex) ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) select { // wait on kill signal case sig := <-ch: - log.Logf("Received signal %s\n", sig) + log.Logf("Received signal %s", sig) // wait on context cancel case <-s.opts.Context.Done(): log.Logf("Received context shutdown") diff --git a/web/service_test.go b/web/service_test.go index 588531b8..33365535 100644 --- a/web/service_test.go +++ b/web/service_test.go @@ -57,10 +57,10 @@ func TestService(t *testing.T) { service.HandleFunc("/", fn) + errCh := make(chan error, 1) go func() { - if err := service.Run(); err != nil { - t.Fatal(err) - } + errCh <- service.Run() + close(errCh) }() var s []*registry.Service @@ -104,12 +104,32 @@ func TestService(t *testing.T) { } } + select { + case err := <-errCh: + if err != nil { + t.Fatalf("service.Run():%v", err) + } + case <-time.After(time.Duration(time.Second)): + t.Logf("service.Run() survived a client request without an error") + } + ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGTERM) syscall.Kill(syscall.Getpid(), syscall.SIGTERM) <-ch + select { + case err := <-errCh: + if err != nil { + t.Fatalf("service.Run():%v", err) + } else { + t.Log("service.Run() nil return on syscall.SIGTERM") + } + case <-time.After(time.Duration(time.Second)): + t.Logf("service.Run() survived a client request without an error") + } + eventually(func() bool { _, err := reg.GetService("go.micro.web.test") return err == registry.ErrNotFound @@ -128,6 +148,7 @@ func TestService(t *testing.T) { t.Errorf("unexpected %s: want true, have false", tt.subject) } } + } func TestOptions(t *testing.T) { @@ -221,10 +242,10 @@ func TestTLS(t *testing.T) { service.HandleFunc("/", fn) + errCh := make(chan error, 1) go func() { - if err := service.Run(); err != nil { - t.Fatal(err) - } + errCh <- service.Run() + close(errCh) }() var s []*registry.Service @@ -257,4 +278,14 @@ func TestTLS(t *testing.T) { if string(b) != str { t.Errorf("Expected %s got %s", str, string(b)) } + + select { + case err := <-errCh: + if err != nil { + t.Fatalf("service.Run():%v", err) + } + case <-time.After(time.Duration(time.Second)): + t.Logf("service.Run() survived a client request without an error") + } + }