chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
examplePattern = regexp.MustCompile(`@example\s+([\s\S]+?)(?:\n\s*\n|$)`)
|
||||
)
|
||||
|
||||
// extractMethodDoc extracts documentation from a method's Go doc comment
|
||||
func extractMethodDoc(method reflect.Method, rcvrType reflect.Type) (description, example string) {
|
||||
// Get the function's source location
|
||||
fn := method.Func
|
||||
if !fn.IsValid() {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
pc := fn.Pointer()
|
||||
if pc == 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Get the source file location
|
||||
funcForPC := runtime.FuncForPC(pc)
|
||||
if funcForPC == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
file, _ := funcForPC.FileLine(pc)
|
||||
if file == "" {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Parse the source file
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Find the receiver type name (e.g., "Users" from *Users)
|
||||
rcvrTypeName := rcvrType.Name()
|
||||
if rcvrTypeName == "" && rcvrType.Kind() == reflect.Pointer {
|
||||
rcvrTypeName = rcvrType.Elem().Name()
|
||||
}
|
||||
|
||||
// Search for the method in the AST
|
||||
for _, decl := range f.Decls {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a method (has receiver)
|
||||
if funcDecl.Recv == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if method name matches
|
||||
if funcDecl.Name.Name != method.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if receiver type matches
|
||||
if len(funcDecl.Recv.List) > 0 {
|
||||
recvTypeName := getTypeName(funcDecl.Recv.List[0].Type)
|
||||
if recvTypeName != rcvrTypeName {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Found the method! Extract its doc comment
|
||||
if funcDecl.Doc != nil {
|
||||
comment := funcDecl.Doc.Text()
|
||||
return parseComment(comment)
|
||||
}
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// getTypeName extracts the type name from an AST expression
|
||||
func getTypeName(expr ast.Expr) string {
|
||||
switch t := expr.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.StarExpr:
|
||||
return getTypeName(t.X)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// parseComment extracts description and example from a doc comment
|
||||
func parseComment(comment string) (description, example string) {
|
||||
// Extract @example if present
|
||||
if match := examplePattern.FindStringSubmatch(comment); len(match) > 1 {
|
||||
example = strings.TrimSpace(match[1])
|
||||
// Remove @example section from description
|
||||
comment = examplePattern.ReplaceAllString(comment, "")
|
||||
}
|
||||
|
||||
// Clean up the description
|
||||
description = strings.TrimSpace(comment)
|
||||
|
||||
// Use doc.Synopsis for the first sentence if description is long
|
||||
if len(description) > 200 {
|
||||
synopsis := doc.Synopsis(description)
|
||||
if synopsis != "" {
|
||||
description = synopsis
|
||||
}
|
||||
}
|
||||
|
||||
return description, example
|
||||
}
|
||||
|
||||
// extractHandlerDocs extracts documentation for all methods of a handler
|
||||
func extractHandlerDocs(handler interface{}) map[string]map[string]string {
|
||||
metadata := make(map[string]map[string]string)
|
||||
|
||||
typ := reflect.TypeOf(handler)
|
||||
if typ == nil {
|
||||
return metadata
|
||||
}
|
||||
|
||||
// Get the receiver type for methods
|
||||
rcvrType := typ
|
||||
if rcvrType.Kind() == reflect.Pointer {
|
||||
rcvrType = rcvrType.Elem()
|
||||
}
|
||||
|
||||
// Iterate through methods
|
||||
for i := 0; i < typ.NumMethod(); i++ {
|
||||
method := typ.Method(i)
|
||||
|
||||
// Skip non-exported methods
|
||||
if method.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract documentation from source
|
||||
description, example := extractMethodDoc(method, rcvrType)
|
||||
|
||||
if description != "" || example != "" {
|
||||
metadata[method.Name] = make(map[string]string)
|
||||
if description != "" {
|
||||
metadata[method.Name]["description"] = description
|
||||
}
|
||||
if example != "" {
|
||||
metadata[method.Name]["example"] = example
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestService is a test service with documented methods
|
||||
type TestService struct{}
|
||||
|
||||
// GetItem retrieves an item by ID. Returns the item if found, error otherwise.
|
||||
//
|
||||
// @example {"id": "item-123"}
|
||||
func (s *TestService) GetItem(ctx context.Context, req *TestRequest, rsp *TestResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateItem creates a new item in the system.
|
||||
//
|
||||
// @example {"name": "New Item", "value": 42}
|
||||
func (s *TestService) CreateItem(ctx context.Context, req *TestRequest, rsp *TestResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TestService) NoDoc(ctx context.Context, req *TestRequest, rsp *TestResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestRequest struct{}
|
||||
type TestResponse struct{}
|
||||
|
||||
func TestExtractHandlerDocs(t *testing.T) {
|
||||
handler := &TestService{}
|
||||
docs := extractHandlerDocs(handler)
|
||||
|
||||
// Test GetItem extraction
|
||||
if docs["GetItem"] == nil {
|
||||
t.Fatal("GetItem documentation not extracted")
|
||||
}
|
||||
if docs["GetItem"]["description"] == "" {
|
||||
t.Error("GetItem description is empty")
|
||||
}
|
||||
if docs["GetItem"]["example"] != `{"id": "item-123"}` {
|
||||
t.Errorf("GetItem example = %q, want %q", docs["GetItem"]["example"], `{"id": "item-123"}`)
|
||||
}
|
||||
|
||||
// Test CreateItem extraction
|
||||
if docs["CreateItem"] == nil {
|
||||
t.Fatal("CreateItem documentation not extracted")
|
||||
}
|
||||
if docs["CreateItem"]["description"] == "" {
|
||||
t.Error("CreateItem description is empty")
|
||||
}
|
||||
if docs["CreateItem"]["example"] != `{"name": "New Item", "value": 42}` {
|
||||
t.Errorf("CreateItem example = %q, want %q", docs["CreateItem"]["example"], `{"name": "New Item", "value": 42}`)
|
||||
}
|
||||
|
||||
// Test NoDoc (should have no metadata or only empty metadata)
|
||||
if len(docs["NoDoc"]) > 0 {
|
||||
t.Logf("NoDoc metadata: %+v", docs["NoDoc"])
|
||||
// Check if all values are empty
|
||||
allEmpty := true
|
||||
for _, v := range docs["NoDoc"] {
|
||||
if v != "" {
|
||||
allEmpty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allEmpty {
|
||||
t.Error("NoDoc should have no metadata with values")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRpcHandlerAutoExtract(t *testing.T) {
|
||||
handler := NewRpcHandler(&TestService{})
|
||||
rpcHandler := handler.(*RpcHandler)
|
||||
|
||||
// Check that endpoints have metadata
|
||||
var foundGetItem bool
|
||||
for _, ep := range rpcHandler.Endpoints() {
|
||||
if ep.Name == "TestService.GetItem" {
|
||||
foundGetItem = true
|
||||
if ep.Metadata["description"] == "" {
|
||||
t.Error("GetItem endpoint missing description metadata")
|
||||
}
|
||||
if ep.Metadata["example"] != `{"id": "item-123"}` {
|
||||
t.Errorf("GetItem endpoint example = %q, want %q", ep.Metadata["example"], `{"id": "item-123"}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundGetItem {
|
||||
t.Error("GetItem endpoint not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualMetadataOverridesAutoExtract(t *testing.T) {
|
||||
// Manual metadata should take precedence over auto-extracted
|
||||
handler := NewRpcHandler(
|
||||
&TestService{},
|
||||
WithEndpointDocs(map[string]EndpointDoc{
|
||||
"TestService.GetItem": {
|
||||
Description: "Manual override description",
|
||||
Example: `{"id": "manual-123"}`,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
rpcHandler := handler.(*RpcHandler)
|
||||
|
||||
for _, ep := range rpcHandler.Endpoints() {
|
||||
if ep.Name == "TestService.GetItem" {
|
||||
if ep.Metadata["description"] != "Manual override description" {
|
||||
t.Errorf("Manual description not used: got %q", ep.Metadata["description"])
|
||||
}
|
||||
if ep.Metadata["example"] != `{"id": "manual-123"}` {
|
||||
t.Errorf("Manual example not used: got %q", ep.Metadata["example"])
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Error("GetItem endpoint not found")
|
||||
}
|
||||
|
||||
func TestWithEndpointScopes(t *testing.T) {
|
||||
handler := NewRpcHandler(
|
||||
&TestService{},
|
||||
WithEndpointScopes("TestService.GetItem", "items:read"),
|
||||
WithEndpointScopes("TestService.CreateItem", "items:write", "items:admin"),
|
||||
)
|
||||
|
||||
rpcHandler := handler.(*RpcHandler)
|
||||
|
||||
var foundGet, foundCreate bool
|
||||
for _, ep := range rpcHandler.Endpoints() {
|
||||
switch ep.Name {
|
||||
case "TestService.GetItem":
|
||||
foundGet = true
|
||||
if ep.Metadata["scopes"] != "items:read" {
|
||||
t.Errorf("GetItem scopes = %q, want %q", ep.Metadata["scopes"], "items:read")
|
||||
}
|
||||
case "TestService.CreateItem":
|
||||
foundCreate = true
|
||||
if ep.Metadata["scopes"] != "items:write,items:admin" {
|
||||
t.Errorf("CreateItem scopes = %q, want %q", ep.Metadata["scopes"], "items:write,items:admin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundGet {
|
||||
t.Error("GetItem endpoint not found")
|
||||
}
|
||||
if !foundCreate {
|
||||
t.Error("CreateItem endpoint not found")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type serverKey struct{}
|
||||
type wgKey struct{}
|
||||
|
||||
func wait(ctx context.Context) *sync.WaitGroup {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
wg, ok := ctx.Value(wgKey{}).(*sync.WaitGroup)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return wg
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) (Server, bool) {
|
||||
c, ok := ctx.Value(serverKey{}).(Server)
|
||||
return c, ok
|
||||
}
|
||||
|
||||
func NewContext(ctx context.Context, s Server) context.Context {
|
||||
return context.WithValue(ctx, serverKey{}, s)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package server
|
||||
|
||||
import "strings"
|
||||
|
||||
// Package server provides options for documenting service endpoints.
|
||||
//
|
||||
// Documentation is AUTOMATICALLY EXTRACTED from Go doc comments on handler methods.
|
||||
// You don't need any extra code - just write good comments!
|
||||
//
|
||||
// Basic usage (automatic):
|
||||
//
|
||||
// // GetUser retrieves a user by ID from the database.
|
||||
// //
|
||||
// // @example {"id": "user-123"}
|
||||
// func (s *UserService) GetUser(ctx context.Context, req *GetUserRequest, rsp *GetUserResponse) error {
|
||||
// // implementation
|
||||
// }
|
||||
//
|
||||
// // Register handler - docs extracted automatically from comments
|
||||
// server.Handle(server.NewHandler(new(UserService)))
|
||||
//
|
||||
// Advanced usage (manual override):
|
||||
//
|
||||
// // Override auto-extracted docs with manual metadata
|
||||
// server.Handle(
|
||||
// server.NewHandler(
|
||||
// new(UserService),
|
||||
// server.WithEndpointDocs(map[string]server.EndpointDoc{
|
||||
// "UserService.GetUser": {
|
||||
// Description: "Custom description overrides comment",
|
||||
// Example: `{"id": "user-123"}`,
|
||||
// },
|
||||
// }),
|
||||
// ),
|
||||
// )
|
||||
|
||||
// EndpointDoc contains documentation for an endpoint
|
||||
type EndpointDoc struct {
|
||||
Description string // What the endpoint does
|
||||
Example string // Example JSON input
|
||||
}
|
||||
|
||||
// WithEndpointDocs returns a HandlerOption that adds documentation to multiple endpoints.
|
||||
// This metadata is stored in the registry and used by MCP gateway to generate
|
||||
// rich tool descriptions for AI agents.
|
||||
//
|
||||
// This is a convenience wrapper around EndpointMetadata for adding docs to multiple endpoints at once.
|
||||
func WithEndpointDocs(docs map[string]EndpointDoc) HandlerOption {
|
||||
return func(o *HandlerOptions) {
|
||||
if o.Metadata == nil {
|
||||
o.Metadata = make(map[string]map[string]string)
|
||||
}
|
||||
|
||||
for endpoint, doc := range docs {
|
||||
if o.Metadata[endpoint] == nil {
|
||||
o.Metadata[endpoint] = make(map[string]string)
|
||||
}
|
||||
if doc.Description != "" {
|
||||
o.Metadata[endpoint]["description"] = doc.Description
|
||||
}
|
||||
if doc.Example != "" {
|
||||
o.Metadata[endpoint]["example"] = doc.Example
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithEndpointDescription is a convenience function for adding a description to a single endpoint.
|
||||
// For multiple endpoints, use WithEndpointDocs instead.
|
||||
func WithEndpointDescription(endpoint, description string) HandlerOption {
|
||||
return EndpointMetadata(endpoint, map[string]string{
|
||||
"description": description,
|
||||
})
|
||||
}
|
||||
|
||||
// WithEndpointExample is a convenience function for adding an example to a single endpoint.
|
||||
func WithEndpointExample(endpoint, example string) HandlerOption {
|
||||
return EndpointMetadata(endpoint, map[string]string{
|
||||
"example": example,
|
||||
})
|
||||
}
|
||||
|
||||
// WithEndpointScopes sets the required auth scopes for a single endpoint.
|
||||
// Scopes are stored as comma-separated values in endpoint metadata and
|
||||
// enforced by the MCP gateway when an Auth provider is configured.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// server.NewHandler(
|
||||
// new(BlogService),
|
||||
// server.WithEndpointScopes("Blog.Create", "blog:write", "blog:admin"),
|
||||
// server.WithEndpointScopes("Blog.Read", "blog:read"),
|
||||
// )
|
||||
func WithEndpointScopes(endpoint string, scopes ...string) HandlerOption {
|
||||
return EndpointMetadata(endpoint, map[string]string{
|
||||
"scopes": strings.Join(scopes, ","),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
if d == 3 {
|
||||
return nil
|
||||
}
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
arg := ®istry.Value{
|
||||
Name: v.Name(),
|
||||
Type: v.Name(),
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
val := extractValue(f.Type, d+1)
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if we can find a json tag use it
|
||||
if tags := f.Tag.Get("json"); len(tags) > 0 {
|
||||
parts := strings.Split(tags, ",")
|
||||
if parts[0] == "-" || parts[0] == "omitempty" {
|
||||
continue
|
||||
}
|
||||
val.Name = parts[0]
|
||||
} else {
|
||||
val.Name = f.Name
|
||||
}
|
||||
|
||||
// if there's no name default it
|
||||
if len(val.Name) == 0 {
|
||||
val.Name = v.Field(i).Name
|
||||
}
|
||||
|
||||
// still no name then continue
|
||||
if len(val.Name) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
arg.Values = append(arg.Values, val)
|
||||
}
|
||||
case reflect.Slice:
|
||||
p := v.Elem()
|
||||
if p.Kind() == reflect.Pointer {
|
||||
p = p.Elem()
|
||||
}
|
||||
arg.Type = "[]" + p.Name()
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
||||
|
||||
func extractEndpoint(method reflect.Method) *registry.Endpoint {
|
||||
if method.PkgPath != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rspType, reqType reflect.Type
|
||||
var stream bool
|
||||
mt := method.Type
|
||||
|
||||
switch mt.NumIn() {
|
||||
case 3:
|
||||
reqType = mt.In(1)
|
||||
rspType = mt.In(2)
|
||||
case 4:
|
||||
reqType = mt.In(2)
|
||||
rspType = mt.In(3)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
// are we dealing with a stream?
|
||||
switch rspType.Kind() {
|
||||
case reflect.Func, reflect.Interface:
|
||||
stream = true
|
||||
}
|
||||
|
||||
request := extractValue(reqType, 0)
|
||||
response := extractValue(rspType, 0)
|
||||
|
||||
ep := ®istry.Endpoint{
|
||||
Name: method.Name,
|
||||
Request: request,
|
||||
Response: response,
|
||||
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 {
|
||||
var reqType reflect.Type
|
||||
switch typ.NumIn() {
|
||||
case 1:
|
||||
reqType = typ.In(0)
|
||||
case 2:
|
||||
reqType = typ.In(1)
|
||||
case 3:
|
||||
reqType = typ.In(2)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return extractValue(reqType, 0)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type testHandler struct{}
|
||||
|
||||
type testRequest struct{}
|
||||
|
||||
type testResponse struct{}
|
||||
|
||||
func (t *testHandler) Test(ctx context.Context, req *testRequest, rsp *testResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExtractEndpoint(t *testing.T) {
|
||||
handler := &testHandler{}
|
||||
typ := reflect.TypeOf(handler)
|
||||
|
||||
var endpoints []*registry.Endpoint
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
if e := extractEndpoint(typ.Method(m)); e != nil {
|
||||
endpoints = append(endpoints, e)
|
||||
}
|
||||
}
|
||||
|
||||
if i := len(endpoints); i != 1 {
|
||||
t.Errorf("Expected 1 endpoint, have %d", i)
|
||||
}
|
||||
|
||||
if endpoints[0].Name != "Test" {
|
||||
t.Errorf("Expected handler Test, got %s", endpoints[0].Name)
|
||||
}
|
||||
|
||||
if endpoints[0].Request == nil {
|
||||
t.Error("Expected non nil request")
|
||||
}
|
||||
|
||||
if endpoints[0].Response == nil {
|
||||
t.Error("Expected non nil request")
|
||||
}
|
||||
|
||||
if endpoints[0].Request.Name != "testRequest" {
|
||||
t.Errorf("Expected testRequest got %s", endpoints[0].Request.Name)
|
||||
}
|
||||
|
||||
if endpoints[0].Response.Name != "testResponse" {
|
||||
t.Errorf("Expected testResponse got %s", endpoints[0].Response.Name)
|
||||
}
|
||||
|
||||
if endpoints[0].Request.Type != "testRequest" {
|
||||
t.Errorf("Expected testRequest type got %s", endpoints[0].Request.Type)
|
||||
}
|
||||
|
||||
if endpoints[0].Response.Type != "testResponse" {
|
||||
t.Errorf("Expected testResponse type got %s", endpoints[0].Response.Type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/codec/bytes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type jsonCodec struct{}
|
||||
type bytesCodec struct{}
|
||||
type protoCodec struct{}
|
||||
type wrapCodec struct{ encoding.Codec }
|
||||
|
||||
var protojsonMarshaler = protojson.MarshalOptions{
|
||||
UseProtoNames: true,
|
||||
EmitUnpopulated: false,
|
||||
}
|
||||
|
||||
var (
|
||||
defaultGRPCCodecs = map[string]encoding.Codec{
|
||||
"application/json": jsonCodec{},
|
||||
"application/proto": protoCodec{},
|
||||
"application/protobuf": protoCodec{},
|
||||
"application/octet-stream": protoCodec{},
|
||||
"application/grpc": protoCodec{},
|
||||
"application/grpc+json": jsonCodec{},
|
||||
"application/grpc+proto": protoCodec{},
|
||||
"application/grpc+bytes": bytesCodec{},
|
||||
}
|
||||
)
|
||||
|
||||
func (w wrapCodec) String() string {
|
||||
return w.Name()
|
||||
}
|
||||
|
||||
func (w wrapCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
b, ok := v.(*bytes.Frame)
|
||||
if ok {
|
||||
return b.Data, nil
|
||||
}
|
||||
return w.Codec.Marshal(v)
|
||||
}
|
||||
|
||||
func (w wrapCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
b, ok := v.(*bytes.Frame)
|
||||
if ok {
|
||||
b.Data = data
|
||||
return nil
|
||||
}
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return w.Codec.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (protoCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
m, ok := v.(proto.Message)
|
||||
if !ok {
|
||||
return nil, codec.ErrInvalidMessage
|
||||
}
|
||||
return proto.Marshal(m)
|
||||
}
|
||||
|
||||
func (protoCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
m, ok := v.(proto.Message)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
return proto.Unmarshal(data, m)
|
||||
}
|
||||
|
||||
func (protoCodec) Name() string {
|
||||
return "proto"
|
||||
}
|
||||
|
||||
func (jsonCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
if pb, ok := v.(proto.Message); ok {
|
||||
return protojsonMarshaler.Marshal(pb)
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (jsonCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if pb, ok := v.(proto.Message); ok {
|
||||
return protojson.Unmarshal(data, pb)
|
||||
}
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (jsonCodec) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func (bytesCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
b, ok := v.(*[]byte)
|
||||
if !ok {
|
||||
return nil, codec.ErrInvalidMessage
|
||||
}
|
||||
return *b, nil
|
||||
}
|
||||
|
||||
func (bytesCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
b, ok := v.(*[]byte)
|
||||
if !ok {
|
||||
return codec.ErrInvalidMessage
|
||||
}
|
||||
*b = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bytesCodec) Name() string {
|
||||
return "bytes"
|
||||
}
|
||||
|
||||
type grpcCodec struct {
|
||||
// headers
|
||||
id string
|
||||
target string
|
||||
method string
|
||||
endpoint string
|
||||
|
||||
s grpc.ServerStream
|
||||
c encoding.Codec
|
||||
}
|
||||
|
||||
func (g *grpcCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
|
||||
md, _ := metadata.FromIncomingContext(g.s.Context())
|
||||
if m == nil {
|
||||
m = new(codec.Message)
|
||||
}
|
||||
if m.Header == nil {
|
||||
m.Header = make(map[string]string, len(md))
|
||||
}
|
||||
for k, v := range md {
|
||||
m.Header[k] = strings.Join(v, ",")
|
||||
}
|
||||
m.Id = g.id
|
||||
m.Target = g.target
|
||||
m.Method = g.method
|
||||
m.Endpoint = g.endpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *grpcCodec) ReadBody(v interface{}) error {
|
||||
// caller has requested a frame
|
||||
if f, ok := v.(*bytes.Frame); ok {
|
||||
return g.s.RecvMsg(f)
|
||||
}
|
||||
return g.s.RecvMsg(v)
|
||||
}
|
||||
|
||||
func (g *grpcCodec) Write(m *codec.Message, v interface{}) error {
|
||||
// if we don't have a body
|
||||
if v != nil {
|
||||
b, err := g.c.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Body = b
|
||||
}
|
||||
// write the body using the framing codec
|
||||
return g.s.SendMsg(&bytes.Frame{Data: m.Body})
|
||||
}
|
||||
|
||||
func (g *grpcCodec) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *grpcCodec) String() string {
|
||||
return "grpc"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
func setServerOption(k, v interface{}) server.Option {
|
||||
return func(o *server.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v6/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func microError(err *errors.Error) codes.Code {
|
||||
switch err {
|
||||
case nil:
|
||||
return codes.OK
|
||||
}
|
||||
|
||||
switch err.Code {
|
||||
case http.StatusOK:
|
||||
return codes.OK
|
||||
case http.StatusBadRequest:
|
||||
return codes.InvalidArgument
|
||||
case http.StatusRequestTimeout:
|
||||
return codes.DeadlineExceeded
|
||||
case http.StatusNotFound:
|
||||
return codes.NotFound
|
||||
case http.StatusConflict:
|
||||
return codes.AlreadyExists
|
||||
case http.StatusForbidden:
|
||||
return codes.PermissionDenied
|
||||
case http.StatusUnauthorized:
|
||||
return codes.Unauthenticated
|
||||
case http.StatusPreconditionFailed:
|
||||
return codes.FailedPrecondition
|
||||
case http.StatusNotImplemented:
|
||||
return codes.Unimplemented
|
||||
case http.StatusInternalServerError:
|
||||
return codes.Internal
|
||||
case http.StatusServiceUnavailable:
|
||||
return codes.Unavailable
|
||||
}
|
||||
|
||||
return codes.Unknown
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
if d == 3 {
|
||||
return nil
|
||||
}
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
arg := ®istry.Value{
|
||||
Name: v.Name(),
|
||||
Type: v.Name(),
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
f := v.Field(i)
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
val := extractValue(f.Type, d+1)
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if we can find a json tag use it
|
||||
if tags := f.Tag.Get("json"); len(tags) > 0 {
|
||||
parts := strings.Split(tags, ",")
|
||||
if parts[0] == "-" || parts[0] == "omitempty" {
|
||||
continue
|
||||
}
|
||||
val.Name = parts[0]
|
||||
}
|
||||
|
||||
// if there's no name default it
|
||||
if len(val.Name) == 0 {
|
||||
val.Name = v.Field(i).Name
|
||||
}
|
||||
|
||||
arg.Values = append(arg.Values, val)
|
||||
}
|
||||
case reflect.Slice:
|
||||
p := v.Elem()
|
||||
if p.Kind() == reflect.Pointer {
|
||||
p = p.Elem()
|
||||
}
|
||||
arg.Type = "[]" + p.Name()
|
||||
}
|
||||
|
||||
return arg
|
||||
}
|
||||
|
||||
func extractEndpoint(method reflect.Method) *registry.Endpoint {
|
||||
if method.PkgPath != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rspType, reqType reflect.Type
|
||||
var stream bool
|
||||
mt := method.Type
|
||||
|
||||
switch mt.NumIn() {
|
||||
case 3:
|
||||
reqType = mt.In(1)
|
||||
rspType = mt.In(2)
|
||||
case 4:
|
||||
reqType = mt.In(2)
|
||||
rspType = mt.In(3)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
// are we dealing with a stream?
|
||||
switch rspType.Kind() {
|
||||
case reflect.Func, reflect.Interface:
|
||||
stream = true
|
||||
}
|
||||
|
||||
request := extractValue(reqType, 0)
|
||||
response := extractValue(rspType, 0)
|
||||
|
||||
ep := ®istry.Endpoint{
|
||||
Name: method.Name,
|
||||
Request: request,
|
||||
Response: response,
|
||||
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 {
|
||||
var reqType reflect.Type
|
||||
switch typ.NumIn() {
|
||||
case 1:
|
||||
reqType = typ.In(0)
|
||||
case 2:
|
||||
reqType = typ.In(1)
|
||||
case 3:
|
||||
reqType = typ.In(2)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return extractValue(reqType, 0)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type testHandler struct{}
|
||||
|
||||
type testRequest struct{}
|
||||
|
||||
type testResponse struct{}
|
||||
|
||||
func (t *testHandler) Test(ctx context.Context, req *testRequest, rsp *testResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExtractEndpoint(t *testing.T) {
|
||||
handler := &testHandler{}
|
||||
typ := reflect.TypeOf(handler)
|
||||
|
||||
var endpoints []*registry.Endpoint
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
if e := extractEndpoint(typ.Method(m)); e != nil {
|
||||
endpoints = append(endpoints, e)
|
||||
}
|
||||
}
|
||||
|
||||
if i := len(endpoints); i != 1 {
|
||||
t.Errorf("Expected 1 endpoint, have %d", i)
|
||||
}
|
||||
|
||||
if endpoints[0].Name != "Test" {
|
||||
t.Errorf("Expected handler Test, got %s", endpoints[0].Name)
|
||||
}
|
||||
|
||||
if endpoints[0].Request == nil {
|
||||
t.Error("Expected non nil request")
|
||||
}
|
||||
|
||||
if endpoints[0].Response == nil {
|
||||
t.Error("Expected non nil request")
|
||||
}
|
||||
|
||||
if endpoints[0].Request.Name != "testRequest" {
|
||||
t.Errorf("Expected testRequest got %s", endpoints[0].Request.Name)
|
||||
}
|
||||
|
||||
if endpoints[0].Response.Name != "testResponse" {
|
||||
t.Errorf("Expected testResponse got %s", endpoints[0].Response.Name)
|
||||
}
|
||||
|
||||
if endpoints[0].Request.Type != "testRequest" {
|
||||
t.Errorf("Expected testRequest type got %s", endpoints[0].Request.Type)
|
||||
}
|
||||
|
||||
if endpoints[0].Response.Type != "testResponse" {
|
||||
t.Errorf("Expected testResponse type got %s", endpoints[0].Response.Type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
micro "go-micro.dev/v6"
|
||||
"go-micro.dev/v6/client"
|
||||
grpcclient "go-micro.dev/v6/client/grpc"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type SleepRequest struct {
|
||||
DelayMS int `json:"delay_ms"`
|
||||
}
|
||||
|
||||
type SleepResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SleepHandler struct {
|
||||
started chan struct{}
|
||||
}
|
||||
|
||||
func (h *SleepHandler) Sleep(ctx context.Context, req *SleepRequest, rsp *SleepResponse) error {
|
||||
select {
|
||||
case h.started <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
timer := time.NewTimer(time.Duration(req.DelayMS) * time.Millisecond)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
rsp.Message = "ok"
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGracefulStopRejectsNewRPCsButAllowsInFlightRPCs(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
addr := listener.Addr().String()
|
||||
reg := registry.NewMemoryRegistry()
|
||||
handler := &SleepHandler{started: make(chan struct{}, 1)}
|
||||
|
||||
svc := micro.NewService("grace-demo",
|
||||
micro.HandleSignal(false),
|
||||
micro.Registry(reg),
|
||||
micro.Server(NewServer(
|
||||
server.Registry(reg),
|
||||
server.Name("grace-demo"),
|
||||
server.Address(addr),
|
||||
Listener(listener),
|
||||
GracefulStopTimeout(3*time.Second),
|
||||
)),
|
||||
micro.Client(grpcclient.NewClient(
|
||||
client.Registry(reg),
|
||||
client.ContentType("application/grpc+json"),
|
||||
client.DialTimeout(200*time.Millisecond),
|
||||
client.RequestTimeout(5*time.Second),
|
||||
)),
|
||||
)
|
||||
|
||||
if err := svc.Handle(handler); err != nil {
|
||||
t.Fatalf("handle: %v", err)
|
||||
}
|
||||
if err := svc.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
|
||||
stopped := false
|
||||
defer func() {
|
||||
if !stopped {
|
||||
_ = svc.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
longDone := make(chan error, 1)
|
||||
go func() {
|
||||
req := svc.Client().NewRequest("grace-demo", "SleepHandler.Sleep", &SleepRequest{DelayMS: 1000})
|
||||
rsp := &SleepResponse{}
|
||||
longDone <- svc.Client().Call(context.Background(), req, rsp, client.WithAddress(addr))
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-handler.started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for long RPC to start")
|
||||
}
|
||||
|
||||
stopDone := make(chan error, 1)
|
||||
go func() {
|
||||
stopDone <- svc.Stop()
|
||||
}()
|
||||
|
||||
freshReq := svc.Client().NewRequest("grace-demo", "SleepHandler.Sleep", &SleepRequest{DelayMS: 10})
|
||||
freshRsp := &SleepResponse{}
|
||||
var rejectErr error
|
||||
|
||||
deadline := time.Now().Add(300 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
callCtx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
|
||||
err := svc.Client().Call(callCtx, freshReq, freshRsp, client.WithAddress(addr))
|
||||
cancel()
|
||||
if err != nil {
|
||||
rejectErr = err
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
if rejectErr == nil {
|
||||
t.Fatal("expected a new RPC to be rejected shortly after shutdown started")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-longDone:
|
||||
if err != nil {
|
||||
t.Fatalf("long RPC failed during graceful stop: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for in-flight RPC to finish")
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-stopDone:
|
||||
if err != nil {
|
||||
t.Fatalf("stop: %v", err)
|
||||
}
|
||||
stopped = true
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for server stop")
|
||||
}
|
||||
}
|
||||
+1051
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type rpcHandler struct {
|
||||
name string
|
||||
handler interface{}
|
||||
endpoints []*registry.Endpoint
|
||||
opts server.HandlerOptions
|
||||
}
|
||||
|
||||
func newRpcHandler(handler interface{}, opts ...server.HandlerOption) server.Handler {
|
||||
options := server.HandlerOptions{
|
||||
Metadata: make(map[string]map[string]string),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
typ := reflect.TypeOf(handler)
|
||||
hdlr := reflect.ValueOf(handler)
|
||||
name := reflect.Indirect(hdlr).Type().Name()
|
||||
|
||||
var endpoints []*registry.Endpoint
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
if e := extractEndpoint(typ.Method(m)); e != nil {
|
||||
e.Name = name + "." + e.Name
|
||||
|
||||
for k, v := range options.Metadata[e.Name] {
|
||||
e.Metadata[k] = v
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, e)
|
||||
}
|
||||
}
|
||||
|
||||
return &rpcHandler{
|
||||
name: name,
|
||||
handler: handler,
|
||||
endpoints: endpoints,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcHandler) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func (r *rpcHandler) Handler() interface{} {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
func (r *rpcHandler) Endpoints() []*registry.Endpoint {
|
||||
return r.endpoints
|
||||
}
|
||||
|
||||
func (r *rpcHandler) Options() server.HandlerOptions {
|
||||
return r.opts
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
"go-micro.dev/v6/transport"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
)
|
||||
|
||||
type codecsKey struct{}
|
||||
type grpcOptions struct{}
|
||||
type netListener struct{}
|
||||
type maxMsgSizeKey struct{}
|
||||
type maxConnKey struct{}
|
||||
type tlsAuth struct{}
|
||||
type grpcServerKey struct{}
|
||||
type gracefulStopTimeoutKey struct{}
|
||||
|
||||
// gRPC Codec to be used to encode/decode requests for a given content type.
|
||||
func Codec(contentType string, c encoding.Codec) server.Option {
|
||||
return func(o *server.Options) {
|
||||
codecs := make(map[string]encoding.Codec)
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
if v, ok := o.Context.Value(codecsKey{}).(map[string]encoding.Codec); ok && v != nil {
|
||||
codecs = v
|
||||
}
|
||||
codecs[contentType] = c
|
||||
o.Context = context.WithValue(o.Context, codecsKey{}, codecs)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthTLS should be used to setup a secure authentication using TLS.
|
||||
func AuthTLS(t *tls.Config) server.Option {
|
||||
return setServerOption(tlsAuth{}, t)
|
||||
}
|
||||
|
||||
// MaxConn specifies maximum number of max simultaneous connections to server.
|
||||
func MaxConn(n int) server.Option {
|
||||
return setServerOption(maxConnKey{}, n)
|
||||
}
|
||||
|
||||
// Listener specifies the net.Listener to use instead of the default.
|
||||
func Listener(l net.Listener) server.Option {
|
||||
return setServerOption(netListener{}, l)
|
||||
}
|
||||
|
||||
// Server specifies a *grpc.Server to use instead of the default
|
||||
// This is for rare use case where user need to expose grpc.Server for
|
||||
// customization. Please NOTE however user injected grpcServer doesn't support
|
||||
// server Handler abstraction.
|
||||
func Server(srv *grpc.Server) server.Option {
|
||||
return setServerOption(grpcServerKey{}, srv)
|
||||
}
|
||||
|
||||
// Options to be used to configure gRPC options.
|
||||
func Options(opts ...grpc.ServerOption) server.Option {
|
||||
return setServerOption(grpcOptions{}, opts)
|
||||
}
|
||||
|
||||
// MaxMsgSize set the maximum message in bytes the server can receive and
|
||||
// send. Default maximum message size is 4 MB.
|
||||
func MaxMsgSize(s int) server.Option {
|
||||
return setServerOption(maxMsgSizeKey{}, s)
|
||||
}
|
||||
|
||||
// GracefulStopTimeout sets how long Stop waits for active RPCs before forcing Stop.
|
||||
func GracefulStopTimeout(timeout time.Duration) server.Option {
|
||||
return setServerOption(gracefulStopTimeoutKey{}, timeout)
|
||||
}
|
||||
|
||||
func newOptions(opt ...server.Option) server.Options {
|
||||
opts := server.Options{
|
||||
Codecs: make(map[string]codec.NewCodec),
|
||||
Metadata: map[string]string{},
|
||||
Broker: broker.DefaultBroker,
|
||||
Registry: registry.DefaultRegistry,
|
||||
RegisterCheck: server.DefaultRegisterCheck,
|
||||
Transport: transport.DefaultTransport,
|
||||
Address: server.DefaultAddress,
|
||||
Name: server.DefaultName,
|
||||
Id: server.DefaultId,
|
||||
Version: server.DefaultVersion,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/codec/bytes"
|
||||
)
|
||||
|
||||
type rpcRequest struct {
|
||||
service string
|
||||
method string
|
||||
contentType string
|
||||
codec codec.Codec
|
||||
header map[string]string
|
||||
body []byte
|
||||
stream bool
|
||||
payload interface{}
|
||||
}
|
||||
|
||||
type rpcMessage struct {
|
||||
topic string
|
||||
contentType string
|
||||
payload interface{}
|
||||
header map[string]string
|
||||
body []byte
|
||||
codec codec.Codec
|
||||
}
|
||||
|
||||
func (r *rpcRequest) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Service() string {
|
||||
return r.service
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Method() string {
|
||||
return r.method
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Endpoint() string {
|
||||
return r.method
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Codec() codec.Reader {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Header() map[string]string {
|
||||
return r.header
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Read() ([]byte, error) {
|
||||
f := &bytes.Frame{}
|
||||
if err := r.codec.ReadBody(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Data, nil
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Stream() bool {
|
||||
return r.stream
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Body() interface{} {
|
||||
return r.payload
|
||||
}
|
||||
|
||||
func (r *rpcMessage) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *rpcMessage) Topic() string {
|
||||
return r.topic
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/codec"
|
||||
)
|
||||
|
||||
type rpcResponse struct {
|
||||
header map[string]string
|
||||
codec codec.Codec
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Codec() codec.Writer {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcResponse) WriteHeader(hdr map[string]string) {
|
||||
for k, v := range hdr {
|
||||
r.header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Write(b []byte) error {
|
||||
return r.codec.Write(&codec.Message{
|
||||
Header: r.header,
|
||||
Body: b,
|
||||
}, nil)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package grpc
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Meh, we need to get rid of this shit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
var (
|
||||
// Precompute the reflect type for error. Can't use error directly
|
||||
// because Typeof takes an empty interface value. This is annoying.
|
||||
typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
)
|
||||
|
||||
type methodType struct {
|
||||
method reflect.Method
|
||||
ArgType reflect.Type
|
||||
ReplyType reflect.Type
|
||||
ContextType reflect.Type
|
||||
stream bool
|
||||
}
|
||||
|
||||
type service struct {
|
||||
name string // name of service
|
||||
rcvr reflect.Value // receiver of methods for the service
|
||||
typ reflect.Type // type of the receiver
|
||||
method map[string]*methodType // registered methods
|
||||
}
|
||||
|
||||
// server represents an RPC Server.
|
||||
type rServer struct {
|
||||
mu sync.Mutex // protects the serviceMap
|
||||
serviceMap map[string]*service
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// Is this an exported - upper case - name?
|
||||
func isExported(name string) bool {
|
||||
rune, _ := utf8.DecodeRuneInString(name)
|
||||
return unicode.IsUpper(rune)
|
||||
}
|
||||
|
||||
// Is this type exported or a builtin?
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
// PkgPath will be non-empty even for an exported type,
|
||||
// so we need to check the type name as well.
|
||||
return isExported(t.Name()) || t.PkgPath() == ""
|
||||
}
|
||||
|
||||
// prepareEndpoint() returns a methodType for the provided method or nil
|
||||
// in case if the method was unsuitable.
|
||||
func prepareEndpoint(method reflect.Method, log logger.Logger) *methodType {
|
||||
mtype := method.Type
|
||||
mname := method.Name
|
||||
var replyType, argType, contextType reflect.Type
|
||||
var stream bool
|
||||
|
||||
// Endpoint() must be exported.
|
||||
if method.PkgPath != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch mtype.NumIn() {
|
||||
case 3:
|
||||
// assuming streaming
|
||||
argType = mtype.In(2)
|
||||
contextType = mtype.In(1)
|
||||
stream = true
|
||||
case 4:
|
||||
// method that takes a context
|
||||
argType = mtype.In(2)
|
||||
replyType = mtype.In(3)
|
||||
contextType = mtype.In(1)
|
||||
default:
|
||||
log.Logf(logger.ErrorLevel, "method %v of %v has wrong number of ins: %v", mname, mtype, mtype.NumIn())
|
||||
return nil
|
||||
}
|
||||
|
||||
if stream {
|
||||
// check stream type
|
||||
streamType := reflect.TypeOf((*server.Stream)(nil)).Elem()
|
||||
if !argType.Implements(streamType) {
|
||||
log.Logf(logger.ErrorLevel, "%v argument does not implement Streamer interface: %v", mname, argType)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// if not stream check the replyType
|
||||
|
||||
// First arg need not be a pointer.
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
log.Logf(logger.ErrorLevel, "%v argument type not exported: %v", mname, argType)
|
||||
return nil
|
||||
}
|
||||
|
||||
if replyType.Kind() != reflect.Pointer {
|
||||
log.Logf(logger.ErrorLevel, "method %v reply type not a pointer: %v", mname, replyType)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reply type must be exported.
|
||||
if !isExportedOrBuiltinType(replyType) {
|
||||
log.Logf(logger.ErrorLevel, "method %v reply type not exported: %v", mname, replyType)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint() needs one out.
|
||||
if mtype.NumOut() != 1 {
|
||||
log.Logf(logger.ErrorLevel, "method %v has wrong number of outs: %v", mname, mtype.NumOut())
|
||||
return nil
|
||||
}
|
||||
// The return type of the method must be error.
|
||||
if returnType := mtype.Out(0); returnType != typeOfError {
|
||||
log.Logf(logger.ErrorLevel, "method %v returns %v not error", mname, returnType.String())
|
||||
return nil
|
||||
}
|
||||
return &methodType{method: method, ArgType: argType, ReplyType: replyType, ContextType: contextType, stream: stream}
|
||||
}
|
||||
|
||||
func (server *rServer) register(rcvr interface{}) error {
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
log := server.logger
|
||||
if server.serviceMap == nil {
|
||||
server.serviceMap = make(map[string]*service)
|
||||
}
|
||||
s := new(service)
|
||||
s.typ = reflect.TypeOf(rcvr)
|
||||
s.rcvr = reflect.ValueOf(rcvr)
|
||||
sname := reflect.Indirect(s.rcvr).Type().Name()
|
||||
if sname == "" {
|
||||
logger.Fatalf("rpc: no service name for type %v", s.typ.String())
|
||||
}
|
||||
if !isExported(sname) {
|
||||
s := "rpc Register: type " + sname + " is not exported"
|
||||
log.Log(logger.ErrorLevel, s)
|
||||
return errors.New(s)
|
||||
}
|
||||
if _, present := server.serviceMap[sname]; present {
|
||||
return errors.New("rpc: service already defined: " + sname)
|
||||
}
|
||||
s.name = sname
|
||||
s.method = make(map[string]*methodType)
|
||||
|
||||
// Install the methods
|
||||
for m := 0; m < s.typ.NumMethod(); m++ {
|
||||
method := s.typ.Method(m)
|
||||
if mt := prepareEndpoint(method, log); mt != nil {
|
||||
s.method[method.Name] = mt
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.method) == 0 {
|
||||
s := "rpc Register: type " + sname + " has no exported methods of suitable type"
|
||||
log.Log(logger.ErrorLevel, s)
|
||||
return errors.New(s)
|
||||
}
|
||||
server.serviceMap[s.name] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *methodType) prepareContext(ctx context.Context) reflect.Value {
|
||||
if contextv := reflect.ValueOf(ctx); contextv.IsValid() {
|
||||
return contextv
|
||||
}
|
||||
return reflect.Zero(m.ContextType)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6/server"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// rpcStream implements a server side Stream.
|
||||
type rpcStream struct {
|
||||
s grpc.ServerStream
|
||||
request server.Request
|
||||
}
|
||||
|
||||
func (r *rpcStream) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcStream) Error() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcStream) Request() server.Request {
|
||||
return r.request
|
||||
}
|
||||
|
||||
func (r *rpcStream) Context() context.Context {
|
||||
return r.s.Context()
|
||||
}
|
||||
|
||||
func (r *rpcStream) Send(m interface{}) error {
|
||||
return r.s.SendMsg(m)
|
||||
}
|
||||
|
||||
func (r *rpcStream) Recv(m interface{}) error {
|
||||
return r.s.RecvMsg(m)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/errors"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/metadata"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
const (
|
||||
subSig = "func(context.Context, interface{}) error"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
method reflect.Value
|
||||
reqType reflect.Type
|
||||
ctxType reflect.Type
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
topic string
|
||||
rcvr reflect.Value
|
||||
typ reflect.Type
|
||||
subscriber interface{}
|
||||
handlers []*handler
|
||||
endpoints []*registry.Endpoint
|
||||
opts server.SubscriberOptions
|
||||
}
|
||||
|
||||
func newSubscriber(topic string, sub interface{}, opts ...server.SubscriberOption) server.Subscriber {
|
||||
options := server.SubscriberOptions{
|
||||
AutoAck: true,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var endpoints []*registry.Endpoint
|
||||
var handlers []*handler
|
||||
|
||||
if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func {
|
||||
h := &handler{
|
||||
method: reflect.ValueOf(sub),
|
||||
}
|
||||
|
||||
switch typ.NumIn() {
|
||||
case 1:
|
||||
h.reqType = typ.In(0)
|
||||
case 2:
|
||||
h.ctxType = typ.In(0)
|
||||
h.reqType = typ.In(1)
|
||||
}
|
||||
|
||||
handlers = append(handlers, h)
|
||||
|
||||
endpoints = append(endpoints, ®istry.Endpoint{
|
||||
Name: "Func",
|
||||
Request: extractSubValue(typ),
|
||||
Metadata: map[string]string{
|
||||
"topic": topic,
|
||||
"subscriber": "true",
|
||||
},
|
||||
})
|
||||
} else {
|
||||
hdlr := reflect.ValueOf(sub)
|
||||
name := reflect.Indirect(hdlr).Type().Name()
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
method := typ.Method(m)
|
||||
h := &handler{
|
||||
method: method.Func,
|
||||
}
|
||||
|
||||
switch method.Type.NumIn() {
|
||||
case 2:
|
||||
h.reqType = method.Type.In(1)
|
||||
case 3:
|
||||
h.ctxType = method.Type.In(1)
|
||||
h.reqType = method.Type.In(2)
|
||||
}
|
||||
|
||||
handlers = append(handlers, h)
|
||||
|
||||
endpoints = append(endpoints, ®istry.Endpoint{
|
||||
Name: name + "." + method.Name,
|
||||
Request: extractSubValue(method.Type),
|
||||
Metadata: map[string]string{
|
||||
"topic": topic,
|
||||
"subscriber": "true",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &subscriber{
|
||||
rcvr: reflect.ValueOf(sub),
|
||||
typ: reflect.TypeOf(sub),
|
||||
topic: topic,
|
||||
subscriber: sub,
|
||||
handlers: handlers,
|
||||
endpoints: endpoints,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func validateSubscriber(sub server.Subscriber) error {
|
||||
typ := reflect.TypeOf(sub.Subscriber())
|
||||
var argType reflect.Type
|
||||
|
||||
if typ.Kind() == reflect.Func {
|
||||
name := "Func"
|
||||
switch typ.NumIn() {
|
||||
case 2:
|
||||
argType = typ.In(1)
|
||||
default:
|
||||
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s", name, typ.NumIn(), subSig)
|
||||
}
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
|
||||
}
|
||||
if typ.NumOut() != 1 {
|
||||
return fmt.Errorf("subscriber %v has wrong number of outs: %v require signature %s",
|
||||
name, typ.NumOut(), subSig)
|
||||
}
|
||||
if returnType := typ.Out(0); returnType != typeOfError {
|
||||
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
|
||||
}
|
||||
} else {
|
||||
hdlr := reflect.ValueOf(sub.Subscriber())
|
||||
name := reflect.Indirect(hdlr).Type().Name()
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
method := typ.Method(m)
|
||||
|
||||
switch method.Type.NumIn() {
|
||||
case 3:
|
||||
argType = method.Type.In(2)
|
||||
default:
|
||||
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s",
|
||||
name, method.Name, method.Type.NumIn(), subSig)
|
||||
}
|
||||
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
return fmt.Errorf("%v argument type not exported: %v", name, argType)
|
||||
}
|
||||
if method.Type.NumOut() != 1 {
|
||||
return fmt.Errorf(
|
||||
"subscriber %v.%v has wrong number of outs: %v require signature %s",
|
||||
name, method.Name, method.Type.NumOut(), subSig)
|
||||
}
|
||||
if returnType := method.Type.Out(0); returnType != typeOfError {
|
||||
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broker.Handler {
|
||||
return func(p broker.Event) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
g.opts.Logger.Log(logger.ErrorLevel, "panic recovered: ", r)
|
||||
g.opts.Logger.Log(logger.ErrorLevel, string(debug.Stack()))
|
||||
err = errors.InternalServerError("go.micro.server", "panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
msg := p.Message()
|
||||
// if we don't have headers, create empty map
|
||||
if msg.Header == nil {
|
||||
msg.Header = make(map[string]string)
|
||||
}
|
||||
|
||||
ct := msg.Header["Content-Type"]
|
||||
if len(ct) == 0 {
|
||||
msg.Header["Content-Type"] = defaultContentType
|
||||
ct = defaultContentType
|
||||
}
|
||||
cf, err := g.newGRPCCodec(ct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdr := make(map[string]string, len(msg.Header))
|
||||
for k, v := range msg.Header {
|
||||
hdr[k] = v
|
||||
}
|
||||
delete(hdr, "Content-Type")
|
||||
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.Pointer {
|
||||
req = reflect.New(handler.reqType.Elem())
|
||||
} else {
|
||||
req = reflect.New(handler.reqType)
|
||||
isVal = true
|
||||
}
|
||||
if isVal {
|
||||
req = req.Elem()
|
||||
}
|
||||
|
||||
if err = cf.Unmarshal(msg.Body, req.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn := func(ctx context.Context, msg server.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 rerr := returnValues[0].Interface(); rerr != nil {
|
||||
return rerr.(error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := len(opts.SubWrappers); i > 0; i-- {
|
||||
fn = opts.SubWrappers[i-1](fn)
|
||||
}
|
||||
|
||||
if g.wg != nil {
|
||||
g.wg.Add(1)
|
||||
}
|
||||
go func() {
|
||||
if g.wg != nil {
|
||||
defer g.wg.Done()
|
||||
}
|
||||
err := fn(ctx, &rpcMessage{
|
||||
topic: sb.topic,
|
||||
contentType: ct,
|
||||
payload: req.Interface(),
|
||||
header: msg.Header,
|
||||
body: msg.Body,
|
||||
})
|
||||
results <- err
|
||||
}()
|
||||
}
|
||||
var errors []string
|
||||
for i := 0; i < len(sb.handlers); i++ {
|
||||
if rerr := <-results; rerr != nil {
|
||||
errors = append(errors, rerr.Error())
|
||||
}
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscriber) Topic() string {
|
||||
return s.topic
|
||||
}
|
||||
|
||||
func (s *subscriber) Subscriber() interface{} {
|
||||
return s.subscriber
|
||||
}
|
||||
|
||||
func (s *subscriber) Endpoints() []*registry.Endpoint {
|
||||
return s.endpoints
|
||||
}
|
||||
|
||||
func (s *subscriber) Options() server.SubscriberOptions {
|
||||
return s.opts
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// convertCode converts a standard Go error into its canonical code. Note that
|
||||
// this is only used to translate the error returned by the server applications.
|
||||
func convertCode(err error) codes.Code {
|
||||
switch err {
|
||||
case nil:
|
||||
return codes.OK
|
||||
case io.EOF:
|
||||
return codes.OutOfRange
|
||||
case io.ErrClosedPipe, io.ErrNoProgress, io.ErrShortBuffer, io.ErrShortWrite, io.ErrUnexpectedEOF:
|
||||
return codes.FailedPrecondition
|
||||
case os.ErrInvalid:
|
||||
return codes.InvalidArgument
|
||||
case context.Canceled:
|
||||
return codes.Canceled
|
||||
case context.DeadlineExceeded:
|
||||
return codes.DeadlineExceeded
|
||||
}
|
||||
switch {
|
||||
case os.IsExist(err):
|
||||
return codes.AlreadyExists
|
||||
case os.IsNotExist(err):
|
||||
return codes.NotFound
|
||||
case os.IsPermission(err):
|
||||
return codes.PermissionDenied
|
||||
}
|
||||
return codes.Unknown
|
||||
}
|
||||
|
||||
func wait(ctx context.Context) *sync.WaitGroup {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
wg, ok := ctx.Value("wait").(*sync.WaitGroup)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return wg
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package server
|
||||
|
||||
import "context"
|
||||
|
||||
type HandlerOption func(*HandlerOptions)
|
||||
|
||||
type HandlerOptions struct {
|
||||
Metadata map[string]map[string]string
|
||||
Internal bool
|
||||
}
|
||||
|
||||
type SubscriberOption func(*SubscriberOptions)
|
||||
|
||||
type SubscriberOptions struct {
|
||||
Context context.Context
|
||||
Queue string
|
||||
// AutoAck defaults to true. When a handler returns
|
||||
// with a nil error the message is acked.
|
||||
AutoAck bool
|
||||
Internal bool
|
||||
}
|
||||
|
||||
// EndpointMetadata is a Handler option that allows metadata to be added to
|
||||
// individual endpoints.
|
||||
func EndpointMetadata(name string, md map[string]string) HandlerOption {
|
||||
return func(o *HandlerOptions) {
|
||||
o.Metadata[name] = md
|
||||
}
|
||||
}
|
||||
|
||||
// Internal Handler options specifies that a handler is not advertised
|
||||
// to the discovery system. In the future this may also limit request
|
||||
// to the internal network or authorized user.
|
||||
func InternalHandler(b bool) HandlerOption {
|
||||
return func(o *HandlerOptions) {
|
||||
o.Internal = b
|
||||
}
|
||||
}
|
||||
|
||||
// Internal Subscriber options specifies that a subscriber is not advertised
|
||||
// to the discovery system.
|
||||
func InternalSubscriber(b bool) SubscriberOption {
|
||||
return func(o *SubscriberOptions) {
|
||||
o.Internal = b
|
||||
}
|
||||
}
|
||||
func NewSubscriberOptions(opts ...SubscriberOption) SubscriberOptions {
|
||||
opt := SubscriberOptions{
|
||||
AutoAck: true,
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// DisableAutoAck will disable auto acking of messages
|
||||
// after they have been handled.
|
||||
func DisableAutoAck() SubscriberOption {
|
||||
return func(o *SubscriberOptions) {
|
||||
o.AutoAck = false
|
||||
}
|
||||
}
|
||||
|
||||
// Shared queue name distributed messages across subscribers.
|
||||
func SubscriberQueue(n string) SubscriberOption {
|
||||
return func(o *SubscriberOptions) {
|
||||
o.Queue = n
|
||||
}
|
||||
}
|
||||
|
||||
// SubscriberContext set context options to allow broker SubscriberOption passed.
|
||||
func SubscriberContext(ctx context.Context) SubscriberOption {
|
||||
return func(o *SubscriberOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type MockServer struct {
|
||||
Opts server.Options
|
||||
Handlers map[string]server.Handler
|
||||
Subscribers map[string][]server.Subscriber
|
||||
sync.Mutex
|
||||
Running bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ server.Server = NewServer()
|
||||
)
|
||||
|
||||
func newMockServer(opts ...server.Option) *MockServer {
|
||||
var options server.Options
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &MockServer{
|
||||
Opts: options,
|
||||
Handlers: make(map[string]server.Handler),
|
||||
Subscribers: make(map[string][]server.Subscriber),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockServer) Options() server.Options {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.Opts
|
||||
}
|
||||
|
||||
func (m *MockServer) Init(opts ...server.Option) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
for _, o := range opts {
|
||||
o(&m.Opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) Handle(h server.Handler) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if _, ok := m.Handlers[h.Name()]; ok {
|
||||
return errors.New("Handler " + h.Name() + " already exists")
|
||||
}
|
||||
m.Handlers[h.Name()] = h
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) NewHandler(h interface{}, opts ...server.HandlerOption) server.Handler {
|
||||
var options server.HandlerOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &MockHandler{
|
||||
Id: uuid.New().String(),
|
||||
Hdlr: h,
|
||||
Opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockServer) NewSubscriber(topic string, fn interface{}, opts ...server.SubscriberOption) server.Subscriber {
|
||||
var options server.SubscriberOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &MockSubscriber{
|
||||
Id: topic,
|
||||
Sub: fn,
|
||||
Opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockServer) Subscribe(sub server.Subscriber) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
subs := m.Subscribers[sub.Topic()]
|
||||
subs = append(subs, sub)
|
||||
m.Subscribers[sub.Topic()] = subs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) Register() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) Deregister() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) Start() error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.Running {
|
||||
return errors.New("already running")
|
||||
}
|
||||
|
||||
m.Running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) Stop() error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if !m.Running {
|
||||
return errors.New("not running")
|
||||
}
|
||||
|
||||
m.Running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockServer) String() string {
|
||||
return "mock"
|
||||
}
|
||||
|
||||
func NewServer(opts ...server.Option) *MockServer {
|
||||
return newMockServer(opts...)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type MockHandler struct {
|
||||
Opts server.HandlerOptions
|
||||
Hdlr interface{}
|
||||
Id string
|
||||
}
|
||||
|
||||
func (m *MockHandler) Name() string {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
func (m *MockHandler) Handler() interface{} {
|
||||
return m.Hdlr
|
||||
}
|
||||
|
||||
func (m *MockHandler) Endpoints() []*registry.Endpoint {
|
||||
return []*registry.Endpoint{}
|
||||
}
|
||||
|
||||
func (m *MockHandler) Options() server.HandlerOptions {
|
||||
return m.Opts
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type MockSubscriber struct {
|
||||
Opts server.SubscriberOptions
|
||||
Sub interface{}
|
||||
Id string
|
||||
}
|
||||
|
||||
func (m *MockSubscriber) Topic() string {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
func (m *MockSubscriber) Subscriber() interface{} {
|
||||
return m.Sub
|
||||
}
|
||||
|
||||
func (m *MockSubscriber) Endpoints() []*registry.Endpoint {
|
||||
return []*registry.Endpoint{}
|
||||
}
|
||||
|
||||
func (m *MockSubscriber) Options() server.SubscriberOptions {
|
||||
return m.Opts
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
func TestMockServer(t *testing.T) {
|
||||
srv := NewServer(
|
||||
server.Name("mock"),
|
||||
server.Version("latest"),
|
||||
)
|
||||
|
||||
if srv.Options().Name != "mock" {
|
||||
t.Fatalf("Expected name mock, got %s", srv.Options().Name)
|
||||
}
|
||||
|
||||
if srv.Options().Version != "latest" {
|
||||
t.Fatalf("Expected version latest, got %s", srv.Options().Version)
|
||||
}
|
||||
|
||||
srv.Init(server.Version("test"))
|
||||
if srv.Options().Version != "test" {
|
||||
t.Fatalf("Expected version test, got %s", srv.Options().Version)
|
||||
}
|
||||
|
||||
h := srv.NewHandler(func() string { return "foo" })
|
||||
if err := srv.Handle(h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sub := srv.NewSubscriber("test", func() string { return "foo" })
|
||||
if err := srv.Subscribe(sub); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if sub.Topic() != "test" {
|
||||
t.Fatalf("Expected topic test got %s", sub.Topic())
|
||||
}
|
||||
|
||||
if err := srv.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := srv.Register(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := srv.Deregister(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := srv.Stop(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/debug/trace"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
type RouterOptions struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type RouterOption func(o *RouterOptions)
|
||||
|
||||
func NewRouterOptions(opt ...RouterOption) RouterOptions {
|
||||
opts := RouterOptions{
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// WithRouterLogger sets the underline router logger.
|
||||
func WithRouterLogger(l logger.Logger) RouterOption {
|
||||
return func(o *RouterOptions) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Logger logger.Logger
|
||||
|
||||
Broker broker.Broker
|
||||
Registry registry.Registry
|
||||
Tracer trace.Tracer
|
||||
Transport transport.Transport
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
|
||||
// The router for requests
|
||||
Router Router
|
||||
|
||||
// RegisterCheck runs a check function before registering the service
|
||||
RegisterCheck func(context.Context) error
|
||||
Metadata map[string]string
|
||||
|
||||
// TLSConfig specifies tls.Config for secure serving
|
||||
TLSConfig *tls.Config
|
||||
|
||||
Codecs map[string]codec.NewCodec
|
||||
Name string
|
||||
Id string
|
||||
Version string
|
||||
Advertise string
|
||||
Address string
|
||||
HdlrWrappers []HandlerWrapper
|
||||
ListenOptions []transport.ListenOption
|
||||
SubWrappers []SubscriberWrapper
|
||||
// The interval on which to register
|
||||
RegisterInterval time.Duration
|
||||
|
||||
// The register expiry time
|
||||
RegisterTTL time.Duration
|
||||
}
|
||||
|
||||
// NewOptions creates new server options.
|
||||
func NewOptions(opt ...Option) Options {
|
||||
opts := Options{
|
||||
Codecs: make(map[string]codec.NewCodec),
|
||||
Metadata: map[string]string{},
|
||||
RegisterInterval: DefaultRegisterInterval,
|
||||
RegisterTTL: DefaultRegisterTTL,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
if opts.Broker == nil {
|
||||
opts.Broker = broker.DefaultBroker
|
||||
}
|
||||
|
||||
if opts.Registry == nil {
|
||||
opts.Registry = registry.DefaultRegistry
|
||||
}
|
||||
|
||||
if opts.Transport == nil {
|
||||
opts.Transport = transport.DefaultTransport
|
||||
}
|
||||
|
||||
if opts.RegisterCheck == nil {
|
||||
opts.RegisterCheck = DefaultRegisterCheck
|
||||
}
|
||||
|
||||
if len(opts.Address) == 0 {
|
||||
opts.Address = DefaultAddress
|
||||
}
|
||||
|
||||
if len(opts.Name) == 0 {
|
||||
opts.Name = DefaultName
|
||||
}
|
||||
|
||||
if len(opts.Id) == 0 {
|
||||
opts.Id = DefaultId
|
||||
}
|
||||
|
||||
if len(opts.Version) == 0 {
|
||||
opts.Version = DefaultVersion
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// Server name.
|
||||
func Name(n string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = n
|
||||
}
|
||||
}
|
||||
|
||||
// Unique server id.
|
||||
func Id(id string) Option {
|
||||
return func(o *Options) {
|
||||
o.Id = id
|
||||
}
|
||||
}
|
||||
|
||||
// Version of the service.
|
||||
func Version(v string) Option {
|
||||
return func(o *Options) {
|
||||
o.Version = v
|
||||
}
|
||||
}
|
||||
|
||||
// Address to bind to - host:port.
|
||||
func Address(a string) Option {
|
||||
return func(o *Options) {
|
||||
o.Address = a
|
||||
}
|
||||
}
|
||||
|
||||
// The address to advertise for discovery - host:port.
|
||||
func Advertise(a string) Option {
|
||||
return func(o *Options) {
|
||||
o.Advertise = a
|
||||
}
|
||||
}
|
||||
|
||||
// Broker to use for pub/sub.
|
||||
func Broker(b broker.Broker) Option {
|
||||
return func(o *Options) {
|
||||
o.Broker = b
|
||||
}
|
||||
}
|
||||
|
||||
// Codec to use to encode/decode requests for a given content type.
|
||||
func Codec(contentType string, c codec.NewCodec) Option {
|
||||
return func(o *Options) {
|
||||
o.Codecs[contentType] = c
|
||||
}
|
||||
}
|
||||
|
||||
// Context specifies a context for the service.
|
||||
// Can be used to signal shutdown of the service
|
||||
// Can be used for extra option values.
|
||||
func Context(ctx context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// Registry used for discovery.
|
||||
func Registry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
// Tracer mechanism for distributed tracking.
|
||||
func Tracer(t trace.Tracer) Option {
|
||||
return func(o *Options) {
|
||||
o.Tracer = t
|
||||
}
|
||||
}
|
||||
|
||||
// Transport mechanism for communication e.g http, rabbitmq, etc.
|
||||
func Transport(t transport.Transport) Option {
|
||||
return func(o *Options) {
|
||||
o.Transport = t
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata associated with the server.
|
||||
func Metadata(md map[string]string) Option {
|
||||
return func(o *Options) {
|
||||
o.Metadata = md
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCheck run func before registry service.
|
||||
func RegisterCheck(fn func(context.Context) error) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisterCheck = fn
|
||||
}
|
||||
}
|
||||
|
||||
// Register the service with a TTL.
|
||||
func RegisterTTL(t time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisterTTL = t
|
||||
}
|
||||
}
|
||||
|
||||
// Register the service with at interval.
|
||||
func RegisterInterval(t time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.RegisterInterval = t
|
||||
}
|
||||
}
|
||||
|
||||
// TLSConfig specifies a *tls.Config.
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
// set the internal tls
|
||||
o.TLSConfig = t
|
||||
|
||||
// set the default transport if one is not
|
||||
// already set. Required for Init call below.
|
||||
if o.Transport == nil {
|
||||
o.Transport = transport.DefaultTransport
|
||||
}
|
||||
|
||||
// set the transport tls
|
||||
_ = o.Transport.Init(
|
||||
transport.Secure(true),
|
||||
transport.TLSConfig(t),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// WithRouter sets the request router.
|
||||
func WithRouter(r Router) Option {
|
||||
return func(o *Options) {
|
||||
o.Router = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the underline logger.
|
||||
func WithLogger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// Wait tells the server to wait for requests to finish before exiting
|
||||
// If `wg` is nil, server only wait for completion of rpc handler.
|
||||
// For user need finer grained control, pass a concrete `wg` here, server will
|
||||
// wait against it on stop.
|
||||
func Wait(wg *sync.WaitGroup) Option {
|
||||
return func(o *Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
if wg == nil {
|
||||
wg = new(sync.WaitGroup)
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, wgKey{}, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a handler Wrapper to a list of options passed into the server.
|
||||
func WrapHandler(w HandlerWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.HdlrWrappers = append(o.HdlrWrappers, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a subscriber Wrapper to a list of options passed into the server.
|
||||
func WrapSubscriber(w SubscriberWrapper) Option {
|
||||
return func(o *Options) {
|
||||
o.SubWrappers = append(o.SubWrappers, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Add transport.ListenOption to the ListenOptions list, when using it, it will be passed to the
|
||||
// httpTransport.Listen() method.
|
||||
func ListenOption(option transport.ListenOption) Option {
|
||||
return func(o *Options) {
|
||||
o.ListenOptions = append(o.ListenOptions, option)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// source: server.proto
|
||||
|
||||
package go_micro_server
|
||||
|
||||
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 HandleRequest struct {
|
||||
Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
|
||||
Endpoint string `protobuf:"bytes,2,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
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_ad098daeda4239f7, []int{0}
|
||||
}
|
||||
|
||||
func (m *HandleRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HandleRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HandleRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HandleRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *HandleRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HandleRequest.Merge(m, src)
|
||||
}
|
||||
func (m *HandleRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_HandleRequest.Size(m)
|
||||
}
|
||||
func (m *HandleRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HandleRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HandleRequest proto.InternalMessageInfo
|
||||
|
||||
func (m *HandleRequest) GetService() string {
|
||||
if m != nil {
|
||||
return m.Service
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *HandleRequest) GetEndpoint() string {
|
||||
if m != nil {
|
||||
return m.Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *HandleRequest) GetProtocol() string {
|
||||
if m != nil {
|
||||
return m.Protocol
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type HandleResponse struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
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_ad098daeda4239f7, []int{1}
|
||||
}
|
||||
|
||||
func (m *HandleResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_HandleResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *HandleResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_HandleResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *HandleResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_HandleResponse.Merge(m, src)
|
||||
}
|
||||
func (m *HandleResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_HandleResponse.Size(m)
|
||||
}
|
||||
func (m *HandleResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_HandleResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_HandleResponse proto.InternalMessageInfo
|
||||
|
||||
type SubscribeRequest struct {
|
||||
Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,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_ad098daeda4239f7, []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 ""
|
||||
}
|
||||
|
||||
type SubscribeResponse struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
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_ad098daeda4239f7, []int{3}
|
||||
}
|
||||
|
||||
func (m *SubscribeResponse) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SubscribeResponse.Unmarshal(m, b)
|
||||
}
|
||||
func (m *SubscribeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_SubscribeResponse.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (m *SubscribeResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_SubscribeResponse.Merge(m, src)
|
||||
}
|
||||
func (m *SubscribeResponse) XXX_Size() int {
|
||||
return xxx_messageInfo_SubscribeResponse.Size(m)
|
||||
}
|
||||
func (m *SubscribeResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_SubscribeResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_SubscribeResponse proto.InternalMessageInfo
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*HandleRequest)(nil), "go.micro.server.HandleRequest")
|
||||
proto.RegisterType((*HandleResponse)(nil), "go.micro.server.HandleResponse")
|
||||
proto.RegisterType((*SubscribeRequest)(nil), "go.micro.server.SubscribeRequest")
|
||||
proto.RegisterType((*SubscribeResponse)(nil), "go.micro.server.SubscribeResponse")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("server.proto", fileDescriptor_ad098daeda4239f7) }
|
||||
|
||||
var fileDescriptor_ad098daeda4239f7 = []byte{
|
||||
// 217 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0x4e, 0x2d, 0x2a,
|
||||
0x4b, 0x2d, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x4f, 0xcf, 0xd7, 0xcb, 0xcd, 0x4c,
|
||||
0x2e, 0xca, 0xd7, 0x83, 0x08, 0x2b, 0x25, 0x72, 0xf1, 0x7a, 0x24, 0xe6, 0xa5, 0xe4, 0xa4, 0x06,
|
||||
0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x49, 0x70, 0xb1, 0x83, 0xa4, 0x32, 0x93, 0x53, 0x25,
|
||||
0x18, 0x15, 0x18, 0x35, 0x38, 0x83, 0x60, 0x5c, 0x21, 0x29, 0x2e, 0x8e, 0xd4, 0xbc, 0x94, 0x82,
|
||||
0xfc, 0xcc, 0xbc, 0x12, 0x09, 0x26, 0xb0, 0x14, 0x9c, 0x0f, 0x92, 0x03, 0x5b, 0x90, 0x9c, 0x9f,
|
||||
0x23, 0xc1, 0x0c, 0x91, 0x83, 0xf1, 0x95, 0x04, 0xb8, 0xf8, 0x60, 0x56, 0x14, 0x17, 0xe4, 0xe7,
|
||||
0x15, 0xa7, 0x2a, 0x69, 0x70, 0x09, 0x04, 0x97, 0x26, 0x15, 0x27, 0x17, 0x65, 0x26, 0xc1, 0xed,
|
||||
0x15, 0xe1, 0x62, 0x2d, 0xc9, 0x2f, 0xc8, 0x4c, 0x86, 0xda, 0x0a, 0xe1, 0x28, 0x09, 0x73, 0x09,
|
||||
0x22, 0xa9, 0x84, 0x68, 0x37, 0x5a, 0xcd, 0xc8, 0xc5, 0x16, 0x0c, 0x76, 0xbe, 0x90, 0x37, 0x17,
|
||||
0x1b, 0xc4, 0x6c, 0x21, 0x39, 0x3d, 0x34, 0xaf, 0xe9, 0xa1, 0xf8, 0x4b, 0x4a, 0x1e, 0xa7, 0x3c,
|
||||
0xd4, 0x51, 0x0c, 0x42, 0x21, 0x5c, 0x9c, 0x70, 0xcb, 0x84, 0x14, 0x31, 0xd4, 0xa3, 0x3b, 0x59,
|
||||
0x4a, 0x09, 0x9f, 0x12, 0x98, 0xa9, 0x49, 0x6c, 0xe0, 0x80, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff,
|
||||
0xff, 0xe0, 0x77, 0x9a, 0xe4, 0x89, 0x01, 0x00, 0x00,
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Code generated by protoc-gen-micro. DO NOT EDIT.
|
||||
// source: server.proto
|
||||
|
||||
package go_micro_server
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
math "math"
|
||||
)
|
||||
|
||||
import (
|
||||
context "context"
|
||||
client "go-micro.dev/v6/client"
|
||||
server "go-micro.dev/v6/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 Server service
|
||||
|
||||
type ServerService interface {
|
||||
Handle(ctx context.Context, in *HandleRequest, opts ...client.CallOption) (*HandleResponse, error)
|
||||
Subscribe(ctx context.Context, in *SubscribeRequest, opts ...client.CallOption) (*SubscribeResponse, error)
|
||||
}
|
||||
|
||||
type serverService struct {
|
||||
c client.Client
|
||||
name string
|
||||
}
|
||||
|
||||
func NewServerService(name string, c client.Client) ServerService {
|
||||
return &serverService{
|
||||
c: c,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *serverService) Handle(ctx context.Context, in *HandleRequest, opts ...client.CallOption) (*HandleResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Server.Handle", in)
|
||||
out := new(HandleResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *serverService) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...client.CallOption) (*SubscribeResponse, error) {
|
||||
req := c.c.NewRequest(c.name, "Server.Subscribe", in)
|
||||
out := new(SubscribeResponse)
|
||||
err := c.c.Call(ctx, req, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Server service
|
||||
|
||||
type ServerHandler interface {
|
||||
Handle(context.Context, *HandleRequest, *HandleResponse) error
|
||||
Subscribe(context.Context, *SubscribeRequest, *SubscribeResponse) error
|
||||
}
|
||||
|
||||
func RegisterServerHandler(s server.Server, hdlr ServerHandler, opts ...server.HandlerOption) error {
|
||||
type server interface {
|
||||
Handle(ctx context.Context, in *HandleRequest, out *HandleResponse) error
|
||||
Subscribe(ctx context.Context, in *SubscribeRequest, out *SubscribeResponse) error
|
||||
}
|
||||
type Server struct {
|
||||
server
|
||||
}
|
||||
h := &serverHandler{hdlr}
|
||||
return s.Handle(s.NewHandler(&Server{h}, opts...))
|
||||
}
|
||||
|
||||
type serverHandler struct {
|
||||
ServerHandler
|
||||
}
|
||||
|
||||
func (h *serverHandler) Handle(ctx context.Context, in *HandleRequest, out *HandleResponse) error {
|
||||
return h.ServerHandler.Handle(ctx, in, out)
|
||||
}
|
||||
|
||||
func (h *serverHandler) Subscribe(ctx context.Context, in *SubscribeRequest, out *SubscribeResponse) error {
|
||||
return h.ServerHandler.Subscribe(ctx, in, out)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package go.micro.server;
|
||||
|
||||
service Server {
|
||||
rpc Handle(HandleRequest) returns (HandleResponse) {};
|
||||
rpc Subscribe(SubscribeRequest) returns (SubscribeResponse) {};
|
||||
}
|
||||
|
||||
message HandleRequest {
|
||||
string service = 1;
|
||||
string endpoint = 2;
|
||||
string protocol = 3;
|
||||
}
|
||||
|
||||
message HandleResponse {}
|
||||
|
||||
message SubscribeRequest {
|
||||
string topic = 1;
|
||||
}
|
||||
|
||||
message SubscribeResponse {}
|
||||
@@ -0,0 +1,362 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
|
||||
"github.com/oxtoacart/bpool"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
raw "go-micro.dev/v6/codec/bytes"
|
||||
"go-micro.dev/v6/codec/grpc"
|
||||
"go-micro.dev/v6/codec/json"
|
||||
"go-micro.dev/v6/codec/jsonrpc"
|
||||
"go-micro.dev/v6/codec/proto"
|
||||
"go-micro.dev/v6/codec/protorpc"
|
||||
"go-micro.dev/v6/transport"
|
||||
"go-micro.dev/v6/transport/headers"
|
||||
)
|
||||
|
||||
type rpcCodec struct {
|
||||
socket transport.Socket
|
||||
codec codec.Codec
|
||||
|
||||
req *transport.Message
|
||||
buf *readWriteCloser
|
||||
|
||||
first chan bool
|
||||
protocol string
|
||||
|
||||
// check if we're the first
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type readWriteCloser struct {
|
||||
wbuf *bytes.Buffer
|
||||
rbuf *bytes.Buffer
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultContentType is the default codec content type.
|
||||
DefaultContentType = "application/protobuf"
|
||||
|
||||
DefaultCodecs = map[string]codec.NewCodec{
|
||||
"application/grpc": grpc.NewCodec,
|
||||
"application/grpc+json": grpc.NewCodec,
|
||||
"application/grpc+proto": grpc.NewCodec,
|
||||
"application/json": json.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/protobuf": proto.NewCodec,
|
||||
"application/proto-rpc": protorpc.NewCodec,
|
||||
"application/octet-stream": raw.NewCodec,
|
||||
}
|
||||
|
||||
// TODO: remove legacy codec list.
|
||||
defaultCodecs = map[string]codec.NewCodec{
|
||||
"application/json": jsonrpc.NewCodec,
|
||||
"application/json-rpc": jsonrpc.NewCodec,
|
||||
"application/protobuf": protorpc.NewCodec,
|
||||
"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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHeader(hdr string, md map[string]string) string {
|
||||
if hd := md[hdr]; len(hd) > 0 {
|
||||
return hd
|
||||
}
|
||||
|
||||
return md["X-"+hdr]
|
||||
}
|
||||
|
||||
func getHeaders(m *codec.Message) {
|
||||
set := func(v, hdr string) string {
|
||||
if len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
|
||||
return m.Header[hdr]
|
||||
}
|
||||
|
||||
m.Id = set(m.Id, headers.ID)
|
||||
m.Error = set(m.Error, headers.Error)
|
||||
m.Endpoint = set(m.Endpoint, headers.Endpoint)
|
||||
m.Method = set(m.Method, headers.Method)
|
||||
m.Target = set(m.Target, headers.Request)
|
||||
|
||||
// TODO: remove this cruft
|
||||
if len(m.Endpoint) == 0 {
|
||||
m.Endpoint = m.Method
|
||||
}
|
||||
}
|
||||
|
||||
func setHeaders(m, r *codec.Message) {
|
||||
set := func(hdr, v string) {
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.Header[hdr] = v
|
||||
m.Header["X-"+hdr] = v
|
||||
}
|
||||
|
||||
// set headers
|
||||
set(headers.ID, r.Id)
|
||||
set(headers.Request, r.Target)
|
||||
set(headers.Method, r.Method)
|
||||
set(headers.Endpoint, r.Endpoint)
|
||||
set(headers.Error, r.Error)
|
||||
}
|
||||
|
||||
// setupProtocol sets up the old protocol.
|
||||
func setupProtocol(msg *transport.Message) codec.NewCodec {
|
||||
service := getHeader(headers.Request, msg.Header)
|
||||
method := getHeader(headers.Method, msg.Header)
|
||||
endpoint := getHeader(headers.Endpoint, msg.Header)
|
||||
protocol := getHeader(headers.Protocol, msg.Header)
|
||||
target := getHeader(headers.Target, msg.Header)
|
||||
topic := getHeader(headers.Message, 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"]]
|
||||
}
|
||||
|
||||
// old target method specified
|
||||
if len(target) > 0 {
|
||||
return defaultCodecs[msg.Header["Content-Type"]]
|
||||
}
|
||||
|
||||
// no method then set to endpoint
|
||||
if len(method) == 0 {
|
||||
msg.Header[headers.Method] = endpoint
|
||||
}
|
||||
|
||||
// no endpoint then set to method
|
||||
if len(endpoint) == 0 {
|
||||
msg.Header[headers.Endpoint] = method
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newRPCCodec(req *transport.Message, socket transport.Socket, c codec.NewCodec) codec.Codec {
|
||||
rwc := &readWriteCloser{
|
||||
rbuf: bufferPool.Get(),
|
||||
wbuf: bufferPool.Get(),
|
||||
}
|
||||
|
||||
r := &rpcCodec{
|
||||
buf: rwc,
|
||||
codec: c(rwc),
|
||||
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":
|
||||
// write the body
|
||||
rwc.rbuf.Write(req.Body)
|
||||
r.protocol = "grpc"
|
||||
default:
|
||||
// first is not preloaded
|
||||
close(r.first)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *rpcCodec) ReadHeader(r *codec.Message, t codec.MessageType) error {
|
||||
// the initial message
|
||||
mmsg := codec.Message{
|
||||
Header: c.req.Header,
|
||||
Body: c.req.Body,
|
||||
}
|
||||
|
||||
// first message could be pre-loaded
|
||||
select {
|
||||
case <-c.first:
|
||||
// not the first
|
||||
var tm transport.Message
|
||||
|
||||
// read off the socket
|
||||
if err := c.socket.Recv(&tm); err != nil {
|
||||
return err
|
||||
}
|
||||
// reset the read buffer
|
||||
c.buf.rbuf.Reset()
|
||||
|
||||
// write the body to the buffer
|
||||
if _, err := c.buf.rbuf.Write(tm.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the message header
|
||||
mmsg.Header = tm.Header
|
||||
// set the message body
|
||||
mmsg.Body = tm.Body
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// set some internal things
|
||||
getHeaders(&mmsg)
|
||||
|
||||
// read header via codec
|
||||
if err := c.codec.ReadHeader(&mmsg, codec.Request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// fallback for 0.14 and older
|
||||
if len(mmsg.Endpoint) == 0 {
|
||||
mmsg.Endpoint = mmsg.Method
|
||||
}
|
||||
|
||||
// set message
|
||||
*r = mmsg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rpcCodec) ReadBody(b interface{}) error {
|
||||
// don't read empty body
|
||||
if len(c.req.Body) == 0 {
|
||||
return nil
|
||||
}
|
||||
// read raw data
|
||||
if v, ok := b.(*raw.Frame); ok {
|
||||
v.Data = c.req.Body
|
||||
return nil
|
||||
}
|
||||
// decode the usual way
|
||||
return c.codec.ReadBody(b)
|
||||
}
|
||||
|
||||
func (c *rpcCodec) Write(r *codec.Message, b interface{}) error {
|
||||
c.buf.wbuf.Reset()
|
||||
|
||||
// create a new message
|
||||
m := &codec.Message{
|
||||
Target: r.Target,
|
||||
Method: r.Method,
|
||||
Endpoint: r.Endpoint,
|
||||
Id: r.Id,
|
||||
Error: r.Error,
|
||||
Type: r.Type,
|
||||
Header: r.Header,
|
||||
}
|
||||
|
||||
if m.Header == nil {
|
||||
m.Header = map[string]string{}
|
||||
}
|
||||
|
||||
setHeaders(m, r)
|
||||
|
||||
// the body being sent
|
||||
var body []byte
|
||||
|
||||
// is it a raw frame?
|
||||
if v, ok := b.(*raw.Frame); ok {
|
||||
body = v.Data
|
||||
// if we have encoded data just send it
|
||||
} else if len(r.Body) > 0 {
|
||||
body = r.Body
|
||||
// write the body to codec
|
||||
} else if err := c.codec.Write(m, b); err != nil {
|
||||
c.buf.wbuf.Reset()
|
||||
|
||||
// write an error if it failed
|
||||
m.Error = errors.Wrapf(err, "Unable to encode body").Error()
|
||||
m.Header[headers.Error] = m.Error
|
||||
// no body to write
|
||||
if err := c.codec.Write(m, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// set the body
|
||||
body = c.buf.wbuf.Bytes()
|
||||
}
|
||||
|
||||
// Set content type if theres content
|
||||
if len(body) > 0 {
|
||||
m.Header["Content-Type"] = c.req.Header["Content-Type"]
|
||||
}
|
||||
|
||||
// send on the socket
|
||||
return c.socket.Send(&transport.Message{
|
||||
Header: m.Header,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *rpcCodec) Close() error {
|
||||
// close the codec
|
||||
c.codec.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 {
|
||||
return c.protocol
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
// testCodec is a dummy codec that only knows how to encode nil bodies.
|
||||
type testCodec struct {
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
type testSocket struct {
|
||||
local string
|
||||
remote string
|
||||
}
|
||||
|
||||
// TestCodecWriteError simulates what happens when a codec is unable
|
||||
// to encode a message (e.g. a missing branch of an "oneof" message in
|
||||
// protobufs)
|
||||
//
|
||||
// We expect an error to be sent to the socket. Previously the socket
|
||||
// would remain open with no bytes sent, leading to client-side
|
||||
// timeouts.
|
||||
func TestCodecWriteError(t *testing.T) {
|
||||
socket := testSocket{}
|
||||
message := transport.Message{
|
||||
Header: map[string]string{},
|
||||
Body: []byte{},
|
||||
}
|
||||
|
||||
rwc := readWriteCloser{
|
||||
rbuf: new(bytes.Buffer),
|
||||
wbuf: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
c := rpcCodec{
|
||||
buf: &rwc,
|
||||
codec: &testCodec{
|
||||
buf: rwc.wbuf,
|
||||
},
|
||||
req: &message,
|
||||
socket: socket,
|
||||
}
|
||||
|
||||
err := c.Write(&codec.Message{
|
||||
Endpoint: "Service.Endpoint",
|
||||
Id: "0",
|
||||
Error: "",
|
||||
}, "body")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf(`Expected Write to fail; got "%+v" instead`, err)
|
||||
}
|
||||
|
||||
const expectedError = "Unable to encode body: simulating a codec write failure"
|
||||
actualError := rwc.wbuf.String()
|
||||
if actualError != expectedError {
|
||||
t.Fatalf(`Expected error "%+v" in the write buffer, got "%+v" instead`, expectedError, actualError)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *testCodec) ReadHeader(message *codec.Message, typ codec.MessageType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testCodec) ReadBody(dest interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testCodec) Write(message *codec.Message, dest interface{}) error {
|
||||
if dest != nil {
|
||||
return errors.New("simulating a codec write failure")
|
||||
}
|
||||
c.buf.Write([]byte(message.Error))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testCodec) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testCodec) String() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (s testSocket) Local() string {
|
||||
return s.local
|
||||
}
|
||||
|
||||
func (s testSocket) Remote() string {
|
||||
return s.remote
|
||||
}
|
||||
|
||||
func (s testSocket) Recv(message *transport.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s testSocket) Send(message *transport.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s testSocket) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/transport"
|
||||
"go-micro.dev/v6/transport/headers"
|
||||
)
|
||||
|
||||
// event is a broker event we handle on the server transport.
|
||||
type event struct {
|
||||
err error
|
||||
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) Error() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *event) Topic() string {
|
||||
return e.message.Header[headers.Message]
|
||||
}
|
||||
|
||||
func newEvent(msg transport.Message) *event {
|
||||
return &event{
|
||||
message: &broker.Message{
|
||||
Header: msg.Header,
|
||||
Body: msg.Body,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
raw "go-micro.dev/v6/codec/bytes"
|
||||
log "go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/metadata"
|
||||
"go-micro.dev/v6/transport/headers"
|
||||
)
|
||||
|
||||
// HandleEvent handles inbound messages to the service directly.
|
||||
// These events are a result of registering to the topic with the service name.
|
||||
// TODO: handle requests from an event. We won't send a response.
|
||||
func (s *rpcServer) HandleEvent(subscriber string) func(e broker.Event) error {
|
||||
return func(e broker.Event) error {
|
||||
// formatting horrible cruft
|
||||
msg := e.Message()
|
||||
|
||||
if msg.Header == nil {
|
||||
msg.Header = make(map[string]string)
|
||||
}
|
||||
|
||||
contentType, ok := msg.Header["Content-Type"]
|
||||
if !ok || len(contentType) == 0 {
|
||||
msg.Header["Content-Type"] = DefaultContentType
|
||||
contentType = DefaultContentType
|
||||
}
|
||||
|
||||
cf, err := s.newCodec(contentType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header := make(map[string]string, len(msg.Header))
|
||||
for k, v := range msg.Header {
|
||||
header[k] = v
|
||||
}
|
||||
|
||||
// create context
|
||||
ctx := metadata.NewContext(context.Background(), header)
|
||||
|
||||
// TODO: inspect message header for Micro-Service & Micro-Topic
|
||||
rpcMsg := &rpcMessage{
|
||||
topic: msg.Header[headers.Message],
|
||||
contentType: contentType,
|
||||
payload: &raw.Frame{Data: msg.Body},
|
||||
codec: cf,
|
||||
header: msg.Header,
|
||||
body: msg.Body,
|
||||
}
|
||||
|
||||
// if the router is present then execute it
|
||||
r := Router(s.router)
|
||||
if s.opts.Router != nil {
|
||||
// create a wrapped function
|
||||
// create a wrapped function
|
||||
handler := func(ctx context.Context, msg Message) error {
|
||||
return s.opts.Router.ProcessMessage(ctx, subscriber, msg)
|
||||
}
|
||||
|
||||
// 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: func(ctx context.Context, _ string, msg Message) error {
|
||||
return handler(ctx, msg)
|
||||
}}
|
||||
}
|
||||
|
||||
return r.ProcessMessage(ctx, subscriber, rpcMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcServer) NewSubscriber(topic string, sb interface{}, opts ...SubscriberOption) Subscriber {
|
||||
return s.router.NewSubscriber(topic, sb, opts...)
|
||||
}
|
||||
|
||||
func (s *rpcServer) Subscribe(sb Subscriber) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if err := validateSubscriber(sub); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// append to subscribers
|
||||
// subs := s.subscribers[sub.Topic()]
|
||||
// subs = append(subs, sub)
|
||||
// router.subscribers[sub.Topic()] = subs
|
||||
|
||||
s.subscribers[sb] = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// subscribeServer will subscribe the server to the topic with its own name.
|
||||
func (s *rpcServer) subscribeServer(config Options) error {
|
||||
if s.opts.Router != nil && s.subscriber == nil {
|
||||
sub, err := s.opts.Broker.Subscribe(config.Name, s.HandleEvent(config.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the subscriber
|
||||
s.subscriber = sub
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reSubscribe itterates over subscribers and re-subscribes then.
|
||||
func (s *rpcServer) reSubscribe(config Options) {
|
||||
for sb := range s.subscribers {
|
||||
if s.subscribers[sb] != nil {
|
||||
continue
|
||||
}
|
||||
// If we've already created a broker subscription for this topic
|
||||
// (from a different Subscriber entry) then don't create another
|
||||
// broker.Subscribe. We still need to register the subscriber with
|
||||
// the router so it receives dispatched messages.
|
||||
var already bool
|
||||
for other, subs := range s.subscribers {
|
||||
if other.Topic() == sb.Topic() && subs != nil {
|
||||
already = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if already {
|
||||
// register with router only
|
||||
if err := s.router.Subscribe(sb); err != nil {
|
||||
config.Logger.Logf(log.WarnLevel, "Unable to subscribing to topic: %s, error: %s", sb.Topic(), err)
|
||||
continue
|
||||
}
|
||||
// mark this subscriber as having no broker subscription
|
||||
s.subscribers[sb] = nil
|
||||
continue
|
||||
}
|
||||
var opts []broker.SubscribeOption
|
||||
if queue := sb.Options().Queue; len(queue) > 0 {
|
||||
opts = append(opts, broker.Queue(queue))
|
||||
}
|
||||
|
||||
if ctx := sb.Options().Context; ctx != nil {
|
||||
opts = append(opts, broker.SubscribeContext(ctx))
|
||||
}
|
||||
|
||||
if !sb.Options().AutoAck {
|
||||
opts = append(opts, broker.DisableAutoAck())
|
||||
}
|
||||
|
||||
config.Logger.Logf(log.InfoLevel, "Subscribing to topic: %s", sb.Topic())
|
||||
sub, err := config.Broker.Subscribe(sb.Topic(), s.HandleEvent(sb.Topic()), opts...)
|
||||
if err != nil {
|
||||
config.Logger.Logf(log.WarnLevel, "Unable to subscribing to topic: %s, error: %s", sb.Topic(), err)
|
||||
continue
|
||||
}
|
||||
err = s.router.Subscribe(sb)
|
||||
if err != nil {
|
||||
config.Logger.Logf(log.WarnLevel, "Unable to subscribing to topic: %s, error: %s", sb.Topic(), err)
|
||||
_ = sub.Unsubscribe()
|
||||
continue
|
||||
}
|
||||
s.subscribers[sb] = []broker.Subscriber{sub}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
// TestSubscriberNoDuplicates verifies that when multiple subscribers are registered
|
||||
// for the same topic with different queues, each handler is called exactly once
|
||||
// per published message (no duplicate deliveries).
|
||||
func TestSubscriberNoDuplicates(t *testing.T) {
|
||||
// Create a memory broker
|
||||
memBroker := broker.NewMemoryBroker()
|
||||
if err := memBroker.Connect(); err != nil {
|
||||
t.Fatalf("Failed to connect broker: %v", err)
|
||||
}
|
||||
defer memBroker.Disconnect()
|
||||
|
||||
// Create a memory registry
|
||||
memRegistry := registry.NewMemoryRegistry()
|
||||
|
||||
// Create server with memory broker and registry
|
||||
srv := NewRPCServer(
|
||||
Broker(memBroker),
|
||||
Registry(memRegistry),
|
||||
Name("test.service"),
|
||||
Id("test-1"),
|
||||
Address("127.0.0.1:0"),
|
||||
)
|
||||
|
||||
// Track handler invocations
|
||||
var countA, countB, countC int32
|
||||
|
||||
// Handler functions
|
||||
handlerA := func(ctx context.Context, msg *TestMessage) error {
|
||||
atomic.AddInt32(&countA, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
handlerB := func(ctx context.Context, msg *TestMessage) error {
|
||||
atomic.AddInt32(&countB, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
handlerC := func(ctx context.Context, msg *TestMessage) error {
|
||||
atomic.AddInt32(&countC, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register three subscribers with same topic but different queues
|
||||
topic := "EVENT_1"
|
||||
|
||||
subA := srv.NewSubscriber(topic, handlerA, SubscriberQueue("A"))
|
||||
if err := srv.Subscribe(subA); err != nil {
|
||||
t.Fatalf("Failed to subscribe A: %v", err)
|
||||
}
|
||||
|
||||
subB := srv.NewSubscriber(topic, handlerB, SubscriberQueue("B"))
|
||||
if err := srv.Subscribe(subB); err != nil {
|
||||
t.Fatalf("Failed to subscribe B: %v", err)
|
||||
}
|
||||
|
||||
subC := srv.NewSubscriber(topic, handlerC, SubscriberQueue("C"))
|
||||
if err := srv.Subscribe(subC); err != nil {
|
||||
t.Fatalf("Failed to subscribe C: %v", err)
|
||||
}
|
||||
|
||||
// Start the server (this will trigger reSubscribe)
|
||||
if err := srv.Start(); err != nil {
|
||||
t.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
defer srv.Stop()
|
||||
|
||||
// Give server time to establish subscriptions
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Publish a message to the topic
|
||||
if err := memBroker.Publish(topic, &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Micro-Topic": topic,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"value":"test"}`),
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to publish message: %v", err)
|
||||
}
|
||||
|
||||
// Give handlers time to process
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Verify each handler was called exactly once
|
||||
if got := atomic.LoadInt32(&countA); got != 1 {
|
||||
t.Errorf("Handler A called %d times, expected 1", got)
|
||||
}
|
||||
if got := atomic.LoadInt32(&countB); got != 1 {
|
||||
t.Errorf("Handler B called %d times, expected 1", got)
|
||||
}
|
||||
if got := atomic.LoadInt32(&countC); got != 1 {
|
||||
t.Errorf("Handler C called %d times, expected 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubscriberMultipleTopics verifies that subscribers for different topics
|
||||
// each receive their respective messages correctly.
|
||||
func TestSubscriberMultipleTopics(t *testing.T) {
|
||||
// Create a memory broker
|
||||
memBroker := broker.NewMemoryBroker()
|
||||
if err := memBroker.Connect(); err != nil {
|
||||
t.Fatalf("Failed to connect broker: %v", err)
|
||||
}
|
||||
defer memBroker.Disconnect()
|
||||
|
||||
// Create a memory registry
|
||||
memRegistry := registry.NewMemoryRegistry()
|
||||
|
||||
// Create server
|
||||
srv := NewRPCServer(
|
||||
Broker(memBroker),
|
||||
Registry(memRegistry),
|
||||
Name("test.service"),
|
||||
Id("test-2"),
|
||||
Address("127.0.0.1:0"),
|
||||
)
|
||||
|
||||
// Track handler invocations
|
||||
var count1, count2 int32
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// Handler functions
|
||||
handler1 := func(ctx context.Context, msg *TestMessage) error {
|
||||
atomic.AddInt32(&count1, 1)
|
||||
wg.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
handler2 := func(ctx context.Context, msg *TestMessage) error {
|
||||
atomic.AddInt32(&count2, 1)
|
||||
wg.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register subscribers for different topics
|
||||
topic1 := "TOPIC_1"
|
||||
topic2 := "TOPIC_2"
|
||||
|
||||
sub1 := srv.NewSubscriber(topic1, handler1)
|
||||
if err := srv.Subscribe(sub1); err != nil {
|
||||
t.Fatalf("Failed to subscribe to topic1: %v", err)
|
||||
}
|
||||
|
||||
sub2 := srv.NewSubscriber(topic2, handler2)
|
||||
if err := srv.Subscribe(sub2); err != nil {
|
||||
t.Fatalf("Failed to subscribe to topic2: %v", err)
|
||||
}
|
||||
|
||||
// Start the server
|
||||
if err := srv.Start(); err != nil {
|
||||
t.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
defer srv.Stop()
|
||||
|
||||
// Give server time to establish subscriptions
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Publish messages to different topics
|
||||
if err := memBroker.Publish(topic1, &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Micro-Topic": topic1,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"value":"test1"}`),
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to publish to topic1: %v", err)
|
||||
}
|
||||
|
||||
if err := memBroker.Publish(topic2, &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Micro-Topic": topic2,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"value":"test2"}`),
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to publish to topic2: %v", err)
|
||||
}
|
||||
|
||||
// Wait for handlers to be called
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Timeout waiting for handlers to be called")
|
||||
}
|
||||
|
||||
// Verify each handler was called exactly once
|
||||
if got := atomic.LoadInt32(&count1); got != 1 {
|
||||
t.Errorf("Handler 1 called %d times, expected 1", got)
|
||||
}
|
||||
if got := atomic.LoadInt32(&count2); got != 1 {
|
||||
t.Errorf("Handler 2 called %d times, expected 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMessage is a test message type
|
||||
type TestMessage struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type RpcHandler struct {
|
||||
handler interface{}
|
||||
opts HandlerOptions
|
||||
name string
|
||||
endpoints []*registry.Endpoint
|
||||
}
|
||||
|
||||
func NewRpcHandler(handler interface{}, opts ...HandlerOption) Handler {
|
||||
options := HandlerOptions{
|
||||
Metadata: make(map[string]map[string]string),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
typ := reflect.TypeOf(handler)
|
||||
hdlr := reflect.ValueOf(handler)
|
||||
name := reflect.Indirect(hdlr).Type().Name()
|
||||
|
||||
// Auto-extract documentation from Go doc comments
|
||||
autoMetadata := extractHandlerDocs(handler)
|
||||
|
||||
// Merge auto-extracted metadata with manually provided metadata
|
||||
// Manual metadata takes precedence over auto-extracted
|
||||
for endpoint, meta := range autoMetadata {
|
||||
fullName := name + "." + endpoint
|
||||
if options.Metadata[fullName] == nil {
|
||||
options.Metadata[fullName] = make(map[string]string)
|
||||
}
|
||||
// Only add auto-extracted values if not manually provided
|
||||
for k, v := range meta {
|
||||
if _, exists := options.Metadata[fullName][k]; !exists {
|
||||
options.Metadata[fullName][k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var endpoints []*registry.Endpoint
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
if e := extractEndpoint(typ.Method(m)); e != nil {
|
||||
e.Name = name + "." + e.Name
|
||||
|
||||
for k, v := range options.Metadata[e.Name] {
|
||||
e.Metadata[k] = v
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, e)
|
||||
}
|
||||
}
|
||||
|
||||
return &RpcHandler{
|
||||
name: name,
|
||||
handler: handler,
|
||||
endpoints: endpoints,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RpcHandler) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func (r *RpcHandler) Handler() interface{} {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
func (r *RpcHandler) Endpoints() []*registry.Endpoint {
|
||||
return r.endpoints
|
||||
}
|
||||
|
||||
func (r *RpcHandler) Options() HandlerOptions {
|
||||
return r.opts
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
// isRegistered will check if the service has already been registered.
|
||||
func (s *rpcServer) isRegistered() bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.registered
|
||||
}
|
||||
|
||||
// setStarted will set started state safely.
|
||||
func (s *rpcServer) setStarted(b bool) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.started = b
|
||||
}
|
||||
|
||||
// isStarted will check if the service has already been started.
|
||||
func (s *rpcServer) isStarted() bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.started
|
||||
}
|
||||
|
||||
// getWaitgroup returns the global waitgroup safely.
|
||||
func (s *rpcServer) getWg() *sync.WaitGroup {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.wg
|
||||
}
|
||||
|
||||
// setOptsAddr will set the address in the service options safely.
|
||||
func (s *rpcServer) setOptsAddr(addr string) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.opts.Address = addr
|
||||
}
|
||||
|
||||
func (s *rpcServer) getCachedService() *registry.Service {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.rsvc
|
||||
}
|
||||
|
||||
func (s *rpcServer) Options() Options {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.opts
|
||||
}
|
||||
|
||||
// swapAddr swaps the address found in the config and the transport address.
|
||||
func (s *rpcServer) swapAddr(config Options, addr string) string {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
a := config.Address
|
||||
s.opts.Address = addr
|
||||
return a
|
||||
}
|
||||
|
||||
func (s *rpcServer) newCodec(contentType string) (codec.NewCodec, error) {
|
||||
if cf, ok := s.opts.Codecs[contentType]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
if cf, ok := DefaultCodecs[contentType]; ok {
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported Content-Type: %s", contentType)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/internal/util/buf"
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
type rpcRequest struct {
|
||||
socket transport.Socket
|
||||
codec codec.Codec
|
||||
rawBody interface{}
|
||||
header map[string]string
|
||||
service string
|
||||
method string
|
||||
endpoint string
|
||||
contentType string
|
||||
body []byte
|
||||
stream bool
|
||||
first bool
|
||||
}
|
||||
|
||||
type rpcMessage struct {
|
||||
payload interface{}
|
||||
header map[string]string
|
||||
codec codec.NewCodec
|
||||
topic string
|
||||
contentType string
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Codec() codec.Reader {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcRequest) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Service() string {
|
||||
return r.service
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Method() string {
|
||||
return r.method
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Endpoint() string {
|
||||
return r.endpoint
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Header() map[string]string {
|
||||
return r.header
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Body() interface{} {
|
||||
return r.rawBody
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Read() ([]byte, error) {
|
||||
// got a body
|
||||
if r.first {
|
||||
b := r.body
|
||||
r.first = false
|
||||
return b, nil
|
||||
}
|
||||
|
||||
var msg transport.Message
|
||||
err := r.socket.Recv(&msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.header = msg.Header
|
||||
|
||||
return msg.Body, nil
|
||||
}
|
||||
|
||||
func (r *rpcRequest) Stream() bool {
|
||||
return r.stream
|
||||
}
|
||||
|
||||
func (r *rpcMessage) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *rpcMessage) Topic() string {
|
||||
return r.topic
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/transport"
|
||||
)
|
||||
|
||||
type rpcResponse struct {
|
||||
header map[string]string
|
||||
socket transport.Socket
|
||||
codec codec.Codec
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Codec() codec.Writer {
|
||||
return r.codec
|
||||
}
|
||||
|
||||
func (r *rpcResponse) WriteHeader(hdr map[string]string) {
|
||||
for k, v := range hdr {
|
||||
r.header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rpcResponse) Write(b []byte) error {
|
||||
if _, ok := r.header["Content-Type"]; !ok {
|
||||
r.header["Content-Type"] = http.DetectContentType(b)
|
||||
}
|
||||
|
||||
return r.socket.Send(&transport.Message{
|
||||
Header: r.header,
|
||||
Body: b,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
merrors "go-micro.dev/v6/errors"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
errLastStreamResponse = errors.New("EOS")
|
||||
|
||||
// Precompute the reflect type for error. Can't use error directly
|
||||
// because Typeof takes an empty interface value. This is annoying.
|
||||
typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
)
|
||||
|
||||
type methodType struct {
|
||||
ArgType reflect.Type
|
||||
ReplyType reflect.Type
|
||||
ContextType reflect.Type
|
||||
method reflect.Method
|
||||
sync.Mutex // protects counters
|
||||
stream bool
|
||||
}
|
||||
|
||||
type service struct {
|
||||
typ reflect.Type // type of the receiver
|
||||
method map[string]*methodType // registered methods
|
||||
rcvr reflect.Value // receiver of methods for the service
|
||||
name string // name of service
|
||||
}
|
||||
|
||||
type request struct {
|
||||
msg *codec.Message
|
||||
next *request // for free list in Server
|
||||
}
|
||||
|
||||
type response struct {
|
||||
msg *codec.Message
|
||||
next *response // for free list in Server
|
||||
}
|
||||
|
||||
// router represents an RPC router.
|
||||
type router struct {
|
||||
ops RouterOptions
|
||||
|
||||
serviceMap map[string]*service
|
||||
|
||||
freeReq *request
|
||||
|
||||
freeResp *response
|
||||
|
||||
subscribers map[string][]*subscriber
|
||||
|
||||
// handler wrappers
|
||||
hdlrWrappers []HandlerWrapper
|
||||
// subscriber wrappers
|
||||
subWrappers []SubscriberWrapper
|
||||
|
||||
su sync.RWMutex
|
||||
|
||||
mu sync.Mutex // protects the serviceMap
|
||||
|
||||
reqLock sync.Mutex // protects freeReq
|
||||
|
||||
respLock sync.Mutex // protects freeResp
|
||||
}
|
||||
|
||||
// rpcRouter encapsulates functions that become a Router.
|
||||
type rpcRouter struct {
|
||||
h func(context.Context, Request, interface{}) error
|
||||
m func(context.Context, string, Message) error
|
||||
}
|
||||
|
||||
func (r rpcRouter) ProcessMessage(ctx context.Context, subscriber string, msg Message) error {
|
||||
return r.m(ctx, subscriber, msg)
|
||||
}
|
||||
|
||||
func (r rpcRouter) ServeRequest(ctx context.Context, req Request, rsp Response) error {
|
||||
return r.h(ctx, req, rsp)
|
||||
}
|
||||
|
||||
func newRpcRouter(opts ...RouterOption) *router {
|
||||
return &router{
|
||||
ops: NewRouterOptions(opts...),
|
||||
serviceMap: make(map[string]*service),
|
||||
subscribers: make(map[string][]*subscriber),
|
||||
}
|
||||
}
|
||||
|
||||
// Is this an exported - upper case - name?
|
||||
func isExported(name string) bool {
|
||||
rune, _ := utf8.DecodeRuneInString(name)
|
||||
return unicode.IsUpper(rune)
|
||||
}
|
||||
|
||||
// Is this type exported or a builtin?
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
// PkgPath will be non-empty even for an exported type,
|
||||
// so we need to check the type name as well.
|
||||
return isExported(t.Name()) || t.PkgPath() == ""
|
||||
}
|
||||
|
||||
// prepareMethod returns a methodType for the provided method or nil
|
||||
// in case if the method was unsuitable.
|
||||
func prepareMethod(method reflect.Method, logger log.Logger) *methodType {
|
||||
mtype := method.Type
|
||||
mname := method.Name
|
||||
var replyType, argType, contextType reflect.Type
|
||||
var stream bool
|
||||
|
||||
// Method must be exported.
|
||||
if method.PkgPath != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch mtype.NumIn() {
|
||||
case 3:
|
||||
// assuming streaming
|
||||
argType = mtype.In(2)
|
||||
contextType = mtype.In(1)
|
||||
stream = true
|
||||
case 4:
|
||||
// method that takes a context
|
||||
argType = mtype.In(2)
|
||||
replyType = mtype.In(3)
|
||||
contextType = mtype.In(1)
|
||||
default:
|
||||
logger.Logf(log.ErrorLevel, "method %v of %v has wrong number of ins: %v", mname, mtype, mtype.NumIn())
|
||||
return nil
|
||||
}
|
||||
|
||||
if stream {
|
||||
// check stream type
|
||||
streamType := reflect.TypeOf((*Stream)(nil)).Elem()
|
||||
if !argType.Implements(streamType) {
|
||||
logger.Logf(log.ErrorLevel, "%v argument does not implement Stream interface: %v", mname, argType)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// if not stream check the replyType
|
||||
|
||||
// First arg need not be a pointer.
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
logger.Logf(log.ErrorLevel, "%v argument type not exported: %v", mname, argType)
|
||||
return nil
|
||||
}
|
||||
|
||||
if replyType.Kind() != reflect.Pointer {
|
||||
logger.Logf(log.ErrorLevel, "method %v reply type not a pointer: %v", mname, replyType)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reply type must be exported.
|
||||
if !isExportedOrBuiltinType(replyType) {
|
||||
logger.Logf(log.ErrorLevel, "method %v reply type not exported: %v", mname, replyType)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Method needs one out.
|
||||
if mtype.NumOut() != 1 {
|
||||
logger.Logf(log.ErrorLevel, "method %v has wrong number of outs: %v", mname, mtype.NumOut())
|
||||
return nil
|
||||
}
|
||||
|
||||
// The return type of the method must be error.
|
||||
if returnType := mtype.Out(0); returnType != typeOfError {
|
||||
logger.Logf(log.ErrorLevel, "method %v returns %v not error", mname, returnType.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
return &methodType{method: method, ArgType: argType, ReplyType: replyType, ContextType: contextType, stream: stream}
|
||||
}
|
||||
|
||||
func (router *router) sendResponse(sending sync.Locker,
|
||||
req *request,
|
||||
reply interface{},
|
||||
cc codec.Writer,
|
||||
last bool) error {
|
||||
msg := new(codec.Message)
|
||||
msg.Type = codec.Response
|
||||
resp := router.getResponse()
|
||||
resp.msg = msg
|
||||
|
||||
resp.msg.Id = req.msg.Id
|
||||
|
||||
sending.Lock()
|
||||
err := cc.Write(resp.msg, reply)
|
||||
sending.Unlock()
|
||||
|
||||
router.freeResponse(resp)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *service) call(ctx context.Context,
|
||||
router *router,
|
||||
sending *sync.Mutex,
|
||||
mtype *methodType,
|
||||
req *request,
|
||||
argv, replyv reflect.Value,
|
||||
cc codec.Writer) error {
|
||||
defer router.freeRequest(req)
|
||||
|
||||
function := mtype.method.Func
|
||||
var returnValues []reflect.Value
|
||||
|
||||
r := &rpcRequest{
|
||||
service: req.msg.Target,
|
||||
contentType: req.msg.Header["Content-Type"],
|
||||
method: req.msg.Method,
|
||||
endpoint: req.msg.Endpoint,
|
||||
body: req.msg.Body,
|
||||
header: req.msg.Header,
|
||||
}
|
||||
|
||||
// only set if not nil
|
||||
if argv.IsValid() {
|
||||
r.rawBody = argv.Interface()
|
||||
}
|
||||
|
||||
if !mtype.stream {
|
||||
fn := func(ctx context.Context, req Request, rsp interface{}) error {
|
||||
returnValues = function.Call([]reflect.Value{s.rcvr, mtype.prepareContext(ctx),
|
||||
reflect.ValueOf(argv.Interface()), reflect.ValueOf(rsp)})
|
||||
|
||||
// The return value for the method is an error.
|
||||
if err := returnValues[0].Interface(); err != nil {
|
||||
return err.(error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrap the handler
|
||||
for i := len(router.hdlrWrappers); i > 0; i-- {
|
||||
fn = router.hdlrWrappers[i-1](fn)
|
||||
}
|
||||
|
||||
// execute handler
|
||||
if err := fn(ctx, r, replyv.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send response
|
||||
return router.sendResponse(sending, req, replyv.Interface(), cc, true)
|
||||
}
|
||||
|
||||
// declare a local error to see if we errored out already
|
||||
// keep track of the type, to make sure we return
|
||||
// the same one consistently
|
||||
rawStream := &rpcStream{
|
||||
context: ctx,
|
||||
codec: cc.(codec.Codec),
|
||||
request: r,
|
||||
id: req.msg.Id,
|
||||
}
|
||||
|
||||
// Invoke the method, providing a new value for the reply.
|
||||
fn := func(ctx context.Context, req Request, stream interface{}) error {
|
||||
returnValues = function.Call([]reflect.Value{s.rcvr, mtype.prepareContext(ctx), reflect.ValueOf(stream)})
|
||||
|
||||
if err := returnValues[0].Interface(); err != nil {
|
||||
// the function returned an error, we use that
|
||||
return err.(error)
|
||||
} else if serr := rawStream.Error(); serr == io.EOF || serr == io.ErrUnexpectedEOF {
|
||||
return nil
|
||||
} else {
|
||||
// no error, we send the special EOS error
|
||||
return errLastStreamResponse
|
||||
}
|
||||
}
|
||||
|
||||
// wrap the handler
|
||||
for i := len(router.hdlrWrappers); i > 0; i-- {
|
||||
fn = router.hdlrWrappers[i-1](fn)
|
||||
}
|
||||
|
||||
// client.Stream request
|
||||
r.stream = true
|
||||
|
||||
// execute handler
|
||||
return fn(ctx, r, rawStream)
|
||||
}
|
||||
|
||||
func (m *methodType) prepareContext(ctx context.Context) reflect.Value {
|
||||
if contextv := reflect.ValueOf(ctx); contextv.IsValid() {
|
||||
return contextv
|
||||
}
|
||||
|
||||
return reflect.Zero(m.ContextType)
|
||||
}
|
||||
|
||||
func (router *router) getRequest() *request {
|
||||
router.reqLock.Lock()
|
||||
defer router.reqLock.Unlock()
|
||||
|
||||
req := router.freeReq
|
||||
if req == nil {
|
||||
req = new(request)
|
||||
} else {
|
||||
router.freeReq = req.next
|
||||
*req = request{}
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func (router *router) freeRequest(req *request) {
|
||||
router.reqLock.Lock()
|
||||
defer router.reqLock.Unlock()
|
||||
|
||||
req.next = router.freeReq
|
||||
router.freeReq = req
|
||||
}
|
||||
|
||||
func (router *router) getResponse() *response {
|
||||
router.respLock.Lock()
|
||||
defer router.respLock.Unlock()
|
||||
|
||||
resp := router.freeResp
|
||||
if resp == nil {
|
||||
resp = new(response)
|
||||
} else {
|
||||
router.freeResp = resp.next
|
||||
*resp = response{}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (router *router) freeResponse(resp *response) {
|
||||
router.respLock.Lock()
|
||||
defer router.respLock.Unlock()
|
||||
|
||||
resp.next = router.freeResp
|
||||
router.freeResp = resp
|
||||
}
|
||||
|
||||
func (router *router) readRequest(r Request) (service *service, mtype *methodType, req *request, argv, replyv reflect.Value, keepReading bool, err error) {
|
||||
cc := r.Codec()
|
||||
|
||||
service, mtype, req, keepReading, err = router.readHeader(cc)
|
||||
if err != nil {
|
||||
if !keepReading {
|
||||
return
|
||||
}
|
||||
// discard body
|
||||
_ = cc.ReadBody(nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// is it a streaming request? then we don't read the body
|
||||
if mtype.stream {
|
||||
if cc.(codec.Codec).String() != "grpc" {
|
||||
_ = cc.ReadBody(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Decode the argument value.
|
||||
argIsValue := false // if true, need to indirect before calling.
|
||||
if mtype.ArgType.Kind() == reflect.Pointer {
|
||||
argv = reflect.New(mtype.ArgType.Elem())
|
||||
} else {
|
||||
argv = reflect.New(mtype.ArgType)
|
||||
argIsValue = true
|
||||
}
|
||||
|
||||
// argv guaranteed to be a pointer now.
|
||||
if err = cc.ReadBody(argv.Interface()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if argIsValue {
|
||||
argv = argv.Elem()
|
||||
}
|
||||
|
||||
if !mtype.stream {
|
||||
replyv = reflect.New(mtype.ReplyType.Elem())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (router *router) readHeader(cc codec.Reader) (service *service, mtype *methodType, req *request, keepReading bool, err error) {
|
||||
// Grab the request header.
|
||||
msg := new(codec.Message)
|
||||
msg.Type = codec.Request
|
||||
req = router.getRequest()
|
||||
req.msg = msg
|
||||
|
||||
err = cc.ReadHeader(msg, msg.Type)
|
||||
if err != nil {
|
||||
req = nil
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
return
|
||||
}
|
||||
err = errors.New("rpc: router cannot decode request: " + err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// We read the header successfully. If we see an error now,
|
||||
// we can still recover and move on to the next request.
|
||||
keepReading = true
|
||||
|
||||
serviceMethod := strings.Split(req.msg.Endpoint, ".")
|
||||
if len(serviceMethod) != 2 {
|
||||
err = errors.New("rpc: service/endpoint request ill-formed: " + req.msg.Endpoint)
|
||||
return
|
||||
}
|
||||
|
||||
// Look up the request.
|
||||
router.mu.Lock()
|
||||
service = router.serviceMap[serviceMethod[0]]
|
||||
router.mu.Unlock()
|
||||
|
||||
if service == nil {
|
||||
err = errors.New("rpc: can't find service " + serviceMethod[0])
|
||||
return
|
||||
}
|
||||
|
||||
mtype = service.method[serviceMethod[1]]
|
||||
if mtype == nil {
|
||||
err = errors.New("rpc: can't find method " + serviceMethod[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (router *router) NewHandler(h interface{}, opts ...HandlerOption) Handler {
|
||||
return NewRpcHandler(h, opts...)
|
||||
}
|
||||
|
||||
func (router *router) Handle(h Handler) error {
|
||||
router.mu.Lock()
|
||||
defer router.mu.Unlock()
|
||||
|
||||
if router.serviceMap == nil {
|
||||
router.serviceMap = make(map[string]*service)
|
||||
}
|
||||
|
||||
if len(h.Name()) == 0 {
|
||||
return errors.New("rpc.Handle: handler has no name")
|
||||
}
|
||||
|
||||
if !isExported(h.Name()) {
|
||||
return errors.New("rpc.Handle: type " + h.Name() + " is not exported")
|
||||
}
|
||||
|
||||
rcvr := h.Handler()
|
||||
s := new(service)
|
||||
s.typ = reflect.TypeOf(rcvr)
|
||||
s.rcvr = reflect.ValueOf(rcvr)
|
||||
|
||||
// check name
|
||||
if _, present := router.serviceMap[h.Name()]; present {
|
||||
return errors.New("rpc.Handle: service already defined: " + h.Name())
|
||||
}
|
||||
|
||||
s.name = h.Name()
|
||||
s.method = make(map[string]*methodType)
|
||||
|
||||
// Install the methods
|
||||
for m := 0; m < s.typ.NumMethod(); m++ {
|
||||
method := s.typ.Method(m)
|
||||
if mt := prepareMethod(method, router.ops.Logger); mt != nil {
|
||||
s.method[method.Name] = mt
|
||||
}
|
||||
}
|
||||
|
||||
// Check there are methods
|
||||
if len(s.method) == 0 {
|
||||
return errors.New("rpc Register: type " + s.name + " has no exported methods of suitable type")
|
||||
}
|
||||
|
||||
// save handler
|
||||
router.serviceMap[s.name] = s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (router *router) ServeRequest(ctx context.Context, r Request, rsp Response) error {
|
||||
sending := new(sync.Mutex)
|
||||
service, mtype, req, argv, replyv, keepReading, err := router.readRequest(r)
|
||||
if err != nil {
|
||||
if !keepReading {
|
||||
return err
|
||||
}
|
||||
// send a response if we actually managed to read a header.
|
||||
if req != nil {
|
||||
router.freeRequest(req)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return service.call(ctx, router, sending, mtype, req, argv, replyv, rsp.Codec())
|
||||
}
|
||||
|
||||
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, subscriber string, msg Message) (err error) {
|
||||
defer func() {
|
||||
// recover any panics
|
||||
if r := recover(); r != nil {
|
||||
router.ops.Logger.Logf(log.ErrorLevel, "panic recovered: %v", r)
|
||||
router.ops.Logger.Log(log.ErrorLevel, string(debug.Stack()))
|
||||
err = merrors.InternalServerError("go.micro.server", "panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// get the subscribers by topic
|
||||
router.su.RLock()
|
||||
subs, ok := router.subscribers[subscriber]
|
||||
router.su.RUnlock()
|
||||
if !ok {
|
||||
log.Warnf("Subscriber not found for topic %s", msg.Topic())
|
||||
return nil
|
||||
}
|
||||
|
||||
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.Pointer {
|
||||
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
|
||||
}
|
||||
|
||||
// make request value a pointer, if it's not already
|
||||
reqVal := req.Interface()
|
||||
if req.CanAddr() {
|
||||
reqVal = req.Addr().Interface()
|
||||
}
|
||||
|
||||
// read the body into the handler request value
|
||||
if err = cc.ReadBody(reqVal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the handler which will honor 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
|
||||
}
|
||||
@@ -0,0 +1,890 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/internal/util/addr"
|
||||
"go-micro.dev/v6/internal/util/backoff"
|
||||
mnet "go-micro.dev/v6/internal/util/net"
|
||||
"go-micro.dev/v6/internal/util/socket"
|
||||
log "go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/metadata"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/transport"
|
||||
"go-micro.dev/v6/transport/headers"
|
||||
)
|
||||
|
||||
type rpcServer struct {
|
||||
opts Options
|
||||
// Subscribe to service name
|
||||
subscriber broker.Subscriber
|
||||
// Goal:
|
||||
// router Router
|
||||
router *router
|
||||
exit chan chan error
|
||||
|
||||
handlers map[string]Handler
|
||||
subscribers map[Subscriber][]broker.Subscriber
|
||||
// Graceful exit
|
||||
wg *sync.WaitGroup
|
||||
// Cached service
|
||||
rsvc *registry.Service
|
||||
|
||||
sync.RWMutex
|
||||
// Marks the serve as started
|
||||
started bool
|
||||
// Used for first registration
|
||||
registered bool
|
||||
}
|
||||
|
||||
// NewRPCServer will create a new default RPC server.
|
||||
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),
|
||||
exit: make(chan chan error),
|
||||
wg: wait(options.Context),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcServer) Init(opts ...Option) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&s.opts)
|
||||
}
|
||||
|
||||
// update router if its the default
|
||||
if s.opts.Router == nil {
|
||||
r := newRpcRouter()
|
||||
r.hdlrWrappers = s.opts.HdlrWrappers
|
||||
r.serviceMap = s.router.serviceMap
|
||||
r.subWrappers = s.opts.SubWrappers
|
||||
s.router = r
|
||||
}
|
||||
|
||||
s.rsvc = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeConn serves a single connection.
|
||||
func (s *rpcServer) ServeConn(sock transport.Socket) {
|
||||
logger := s.opts.Logger
|
||||
|
||||
// Global error tracking
|
||||
var gerr error
|
||||
|
||||
// Keep track of Connection: close header
|
||||
var closeConn bool
|
||||
|
||||
// Streams are multiplexed on Micro-Stream or Micro-Id header
|
||||
pool := socket.NewPool()
|
||||
|
||||
// Waitgroup to wait for processing to finish
|
||||
// A double waitgroup is used to block the global waitgroup incase it is
|
||||
// empty, but only wait for the local routines to finish with the local waitgroup.
|
||||
wg := NewWaitGroup(s.getWg())
|
||||
|
||||
defer func() {
|
||||
// Only wait if there's no error
|
||||
if gerr != nil {
|
||||
select {
|
||||
case <-s.exit:
|
||||
default:
|
||||
// EOF is expected if the client closes the connection
|
||||
if !errors.Is(gerr, io.EOF) {
|
||||
logger.Logf(log.ErrorLevel, "error while serving connection: %v", gerr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Close all the sockets for this connection
|
||||
pool.Close()
|
||||
|
||||
// Close underlying socket
|
||||
if err := sock.Close(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "failed to close socket: %v", err)
|
||||
}
|
||||
|
||||
// recover any panics
|
||||
if r := recover(); r != nil {
|
||||
logger.Log(log.ErrorLevel, "panic recovered: ", r)
|
||||
logger.Log(log.ErrorLevel, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
msg := transport.Message{
|
||||
Header: make(map[string]string),
|
||||
}
|
||||
|
||||
// Close connection if Connection: close header was set
|
||||
if closeConn {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 = errors.Wrapf(err, "%s-%s | %s", s.opts.Name, s.opts.Id, sock.Remote())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Keep track of when to close the connection
|
||||
if c := msg.Header["Connection"]; c == "close" {
|
||||
closeConn = true
|
||||
}
|
||||
|
||||
// Check the message header for micro message header, if so handle
|
||||
// as micro event
|
||||
if t := msg.Header[headers.Message]; len(t) > 0 {
|
||||
// Process the event
|
||||
ev := newEvent(msg)
|
||||
|
||||
if err := s.HandleEvent(ev.Topic())(ev); err != nil {
|
||||
msg.Header[headers.Error] = err.Error()
|
||||
logger.Logf(log.ErrorLevel, "failed to handle event: %v", err)
|
||||
}
|
||||
// Write back some 200
|
||||
if err := sock.Send(&transport.Message{Header: msg.Header}); err != nil {
|
||||
gerr = err
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// business as usual
|
||||
|
||||
// use Micro-Stream as the stream identifier
|
||||
// in the event its blank we'll always process
|
||||
// on the same socket
|
||||
var (
|
||||
stream bool
|
||||
id string
|
||||
)
|
||||
|
||||
if s := getHeader(headers.Stream, msg.Header); len(s) > 0 {
|
||||
id = s
|
||||
stream = true
|
||||
} else {
|
||||
// If there's no stream id then its a standard request
|
||||
// use the Micro-Id
|
||||
id = msg.Header[headers.ID]
|
||||
}
|
||||
|
||||
// Check if we have an existing socket
|
||||
psock, ok := pool.Get(id)
|
||||
|
||||
// If we don't have a socket and its a stream
|
||||
// Check if its a last stream EOS error
|
||||
if !ok && stream && msg.Header[headers.Error] == errLastStreamResponse.Error() {
|
||||
closeConn = true
|
||||
pool.Release(psock)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Got an existing socket already
|
||||
if ok {
|
||||
// we're starting processing
|
||||
wg.Add(1)
|
||||
|
||||
// Pass the message to that existing socket
|
||||
if err := psock.Accept(&msg); err != nil {
|
||||
// Release the socket if there's an error
|
||||
pool.Release(psock)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// No socket was found so its new
|
||||
// Set the local and remote values
|
||||
psock.SetLocal(sock.Local())
|
||||
psock.SetRemote(sock.Remote())
|
||||
|
||||
// Load the socket with the current message
|
||||
if err := psock.Accept(&msg); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Socket failed to accept message: %v", err)
|
||||
}
|
||||
|
||||
// Now walk the usual path
|
||||
|
||||
// We use this Timeout header to set a server deadline
|
||||
to := msg.Header["Timeout"]
|
||||
// We use this Content-Type header to identify the codec needed
|
||||
contentType := msg.Header["Content-Type"]
|
||||
|
||||
// Copy the message headers
|
||||
header := make(map[string]string, len(msg.Header))
|
||||
for k, v := range msg.Header {
|
||||
header[k] = v
|
||||
}
|
||||
|
||||
// Set local/remote ips
|
||||
header["Local"] = sock.Local()
|
||||
header["Remote"] = sock.Remote()
|
||||
|
||||
// Create new context with the metadata
|
||||
ctx := metadata.NewContext(context.Background(), header)
|
||||
|
||||
// Set the timeout from the header if we have it
|
||||
if len(to) > 0 {
|
||||
if n, err := strconv.ParseUint(to, 10, 64); err == nil {
|
||||
var cancel context.CancelFunc
|
||||
|
||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(n))
|
||||
defer cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no content type default it
|
||||
if len(contentType) == 0 {
|
||||
msg.Header["Content-Type"] = DefaultContentType
|
||||
contentType = DefaultContentType
|
||||
}
|
||||
|
||||
// Setup old protocol
|
||||
cf := setupProtocol(&msg)
|
||||
|
||||
// No legacy codec needed
|
||||
if cf == nil {
|
||||
var err error
|
||||
// Try get a new codec
|
||||
if cf, err = s.newCodec(contentType); err != nil {
|
||||
// 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()),
|
||||
}); err != nil {
|
||||
gerr = err
|
||||
}
|
||||
|
||||
pool.Release(psock)
|
||||
|
||||
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()
|
||||
|
||||
// Internal request
|
||||
request := rpcRequest{
|
||||
service: getHeader(headers.Request, msg.Header),
|
||||
method: getHeader(headers.Method, msg.Header),
|
||||
endpoint: getHeader(headers.Endpoint, msg.Header),
|
||||
contentType: contentType,
|
||||
codec: rcodec,
|
||||
header: msg.Header,
|
||||
body: msg.Body,
|
||||
socket: psock,
|
||||
stream: stream,
|
||||
}
|
||||
|
||||
// Internal response
|
||||
response := rpcResponse{
|
||||
header: make(map[string]string),
|
||||
socket: psock,
|
||||
codec: rcodec,
|
||||
}
|
||||
|
||||
// Wait for two coroutines to exit
|
||||
// Serve the request and process the outbound messages
|
||||
wg.Add(2)
|
||||
|
||||
// Process the outbound messages from the socket
|
||||
go func(psock *socket.Socket) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Log(log.ErrorLevel, "panic recovered in outbound goroutine: ", r)
|
||||
logger.Log(log.ErrorLevel, string(debug.Stack()))
|
||||
}
|
||||
// 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
|
||||
if protocol == "grpc" {
|
||||
if err := sock.Close(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Failed to close socket: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.deferer(pool, psock, wg)
|
||||
}()
|
||||
|
||||
for {
|
||||
// Get the message from our internal handler/stream
|
||||
m := new(transport.Message)
|
||||
if err := psock.Process(m); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Send the message back over the socket
|
||||
if err := sock.Send(m); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}(psock)
|
||||
|
||||
// Serve the request in a go routine as this may be a stream
|
||||
go func(psock *socket.Socket) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Log(log.ErrorLevel, "panic recovered in serveReq goroutine: ", r)
|
||||
logger.Log(log.ErrorLevel, string(debug.Stack()))
|
||||
}
|
||||
s.deferer(pool, psock, wg)
|
||||
}()
|
||||
|
||||
s.serveReq(ctx, msg, &request, &response, rcodec)
|
||||
}(psock)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcServer) NewHandler(h interface{}, opts ...HandlerOption) Handler {
|
||||
return s.router.NewHandler(h, opts...)
|
||||
}
|
||||
|
||||
func (s *rpcServer) Handle(h Handler) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if err := s.router.Handle(h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.handlers[h.Name()] = h
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rpcServer) Register() error {
|
||||
config := s.Options()
|
||||
logger := config.Logger
|
||||
|
||||
// Registry function used to register the service
|
||||
regFunc := s.newRegFuc(config)
|
||||
|
||||
// Directly register if service was cached
|
||||
rsvc := s.getCachedService()
|
||||
if rsvc != nil {
|
||||
if err := regFunc(rsvc); err != nil {
|
||||
return errors.Wrap(err, "failed to register service")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only cache service if host IP valid
|
||||
addr, cacheService, err := s.getAddr(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node := ®istry.Node{
|
||||
// TODO: node id should be set better. Add native option to specify
|
||||
// host id through either config or ENV. Also look at logging of name.
|
||||
Id: config.Name + "-" + config.Id,
|
||||
Address: addr,
|
||||
Metadata: s.newNodeMetedata(config),
|
||||
}
|
||||
|
||||
service := ®istry.Service{
|
||||
Name: config.Name,
|
||||
Version: config.Version,
|
||||
Nodes: []*registry.Node{node},
|
||||
Endpoints: s.getEndpoints(),
|
||||
}
|
||||
|
||||
registered := s.isRegistered()
|
||||
if !registered {
|
||||
logger.Logf(log.InfoLevel, "Registry [%s] Registering node: %s", config.Registry.String(), node.Id)
|
||||
}
|
||||
|
||||
// Register the service
|
||||
if err := regFunc(service); err != nil {
|
||||
return errors.Wrap(err, "failed to register service")
|
||||
}
|
||||
|
||||
// Already registered? don't need to register subscribers
|
||||
if registered {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.registered = true
|
||||
|
||||
// Cache service
|
||||
if cacheService {
|
||||
s.rsvc = service
|
||||
}
|
||||
|
||||
// Set what we're advertising
|
||||
s.opts.Advertise = addr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rpcServer) Deregister() error {
|
||||
config := s.Options()
|
||||
logger := config.Logger
|
||||
|
||||
addr, _, err := s.getAddr(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: there should be a better way to do this than reconstruct the service
|
||||
// Edge case is that if service is not cached
|
||||
node := ®istry.Node{
|
||||
// TODO: also update node id naming
|
||||
Id: config.Name + "-" + config.Id,
|
||||
Address: addr,
|
||||
}
|
||||
|
||||
service := ®istry.Service{
|
||||
Name: config.Name,
|
||||
Version: config.Version,
|
||||
Nodes: []*registry.Node{node},
|
||||
}
|
||||
|
||||
logger.Logf(log.InfoLevel, "Registry [%s] Deregistering node: %s", config.Registry.String(), node.Id)
|
||||
|
||||
if err := config.Registry.Deregister(service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.rsvc = nil
|
||||
|
||||
if !s.registered {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.registered = false
|
||||
|
||||
// close the subscriber
|
||||
if s.subscriber != nil {
|
||||
if err := s.subscriber.Unsubscribe(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Failed to unsubscribe service from service name topic: %v", err)
|
||||
}
|
||||
|
||||
s.subscriber = nil
|
||||
}
|
||||
|
||||
for sb, subs := range s.subscribers {
|
||||
for i, sub := range subs {
|
||||
logger.Logf(log.InfoLevel, "Unsubscribing %s from topic: %s", node.Id, sub.Topic())
|
||||
|
||||
if err := sub.Unsubscribe(); err != nil {
|
||||
logger.Logf(log.ErrorLevel,
|
||||
"Failed to unsubscribe subscriber nr. %d from topic %s: %v",
|
||||
i+1,
|
||||
sub.Topic(),
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
s.subscribers[sb] = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rpcServer) Start() error {
|
||||
if s.isStarted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := s.Options()
|
||||
logger := config.Logger
|
||||
|
||||
// start listening on the listener
|
||||
listener, err := config.Transport.Listen(config.Address, config.ListenOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Logf(log.InfoLevel, "Transport [%s] Listening on %s", config.Transport.String(), listener.Addr())
|
||||
|
||||
// swap address
|
||||
addr := s.swapAddr(config, listener.Addr())
|
||||
|
||||
// connect to the broker
|
||||
brokerName := config.Broker.String()
|
||||
if err = config.Broker.Connect(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Broker [%s] connect error: %v", brokerName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Logf(log.InfoLevel, "Broker [%s] Connected to %s", brokerName, config.Broker.Address())
|
||||
|
||||
// Use RegisterCheck func before register
|
||||
if err = s.opts.RegisterCheck(s.opts.Context); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s register check error: %s", config.Name, config.Id, err)
|
||||
} else if err = s.Register(); err != nil {
|
||||
// Perform initial registration
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s register error: %s", config.Name, config.Id, err)
|
||||
}
|
||||
|
||||
exit := make(chan bool)
|
||||
|
||||
// Listen for connections
|
||||
go s.listen(listener, exit)
|
||||
|
||||
// Keep the service registered to registry
|
||||
go s.registrar(listener, addr, config, exit)
|
||||
|
||||
s.setStarted(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rpcServer) Stop() error {
|
||||
if !s.isStarted() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ch := make(chan error)
|
||||
s.exit <- ch
|
||||
|
||||
err := <-ch
|
||||
|
||||
s.setStarted(false)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *rpcServer) String() string {
|
||||
return "mucp"
|
||||
}
|
||||
|
||||
// newRegFuc will create a new registry function used to register the service.
|
||||
func (s *rpcServer) newRegFuc(config Options) func(service *registry.Service) error {
|
||||
return func(service *registry.Service) error {
|
||||
rOpts := []registry.RegisterOption{registry.RegisterTTL(config.RegisterTTL)}
|
||||
|
||||
var regErr error
|
||||
|
||||
// Attempt to register. If registration fails, back off and try again.
|
||||
// TODO: see if we can improve the retry mechanism. Maybe retry lib, maybe config values
|
||||
for i := 0; i < 3; i++ {
|
||||
if regErr = config.Registry.Register(service, rOpts...); regErr != nil {
|
||||
time.Sleep(backoff.Do(i + 1))
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if regErr != nil {
|
||||
return regErr
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
// Router can exchange messages on broker
|
||||
// Subscribe to the topic with its own name
|
||||
if err := s.subscribeServer(config); err != nil {
|
||||
return errors.Wrap(err, "failed to subscribe to service name topic")
|
||||
}
|
||||
// Subscribe for all of the subscribers
|
||||
s.reSubscribe(config)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// getAddr will take the advertise or service address, and return it.
|
||||
func (s *rpcServer) getAddr(config Options) (string, bool, error) {
|
||||
// Use advertise address if provided, else use service address
|
||||
advt := config.Address
|
||||
if len(config.Advertise) > 0 {
|
||||
advt = config.Advertise
|
||||
}
|
||||
|
||||
// Use explicit host and port if possible
|
||||
host, port := advt, ""
|
||||
|
||||
if cnt := strings.Count(advt, ":"); cnt >= 1 {
|
||||
// ipv6 address in format [host]:port or ipv4 host:port
|
||||
h, p, err := net.SplitHostPort(advt)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
host, port = h, p
|
||||
}
|
||||
|
||||
validHost := net.ParseIP(host) != nil
|
||||
|
||||
addr, err := addr.Extract(host)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// mq-rpc(eg. nats) doesn't need the port. its addr is queue name.
|
||||
if port != "" {
|
||||
addr = mnet.HostPort(addr, port)
|
||||
}
|
||||
|
||||
return addr, validHost, nil
|
||||
}
|
||||
|
||||
// newNodeMetedata creates a new metadata map with default values.
|
||||
func (s *rpcServer) newNodeMetedata(config Options) metadata.Metadata {
|
||||
md := metadata.Copy(config.Metadata)
|
||||
|
||||
// TODO: revisit this for v5
|
||||
md["transport"] = config.Transport.String()
|
||||
md["broker"] = config.Broker.String()
|
||||
md["server"] = s.String()
|
||||
md["registry"] = config.Registry.String()
|
||||
md["protocol"] = "mucp"
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
// getEndpoints takes the list of handlers and subscribers and adds them to
|
||||
// a single endpoints list.
|
||||
func (s *rpcServer) getEndpoints() []*registry.Endpoint {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
var handlerList []string
|
||||
|
||||
for n, e := range s.handlers {
|
||||
// Only advertise non internal handlers
|
||||
if !e.Options().Internal {
|
||||
handlerList = append(handlerList, n)
|
||||
}
|
||||
}
|
||||
|
||||
// Maps are ordered randomly, sort the keys for consistency
|
||||
// TODO: replace with generic version
|
||||
sort.Strings(handlerList)
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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()...)
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (s *rpcServer) listen(listener transport.Listener, exit chan bool) {
|
||||
for {
|
||||
// Start listening for connections
|
||||
// This will block until either exit signal given or error occurred
|
||||
err := listener.Accept(s.ServeConn)
|
||||
|
||||
// TODO: listen for messages
|
||||
// msg := broker.Exchange(service).Consume()
|
||||
|
||||
select {
|
||||
// check if we're supposed to exit
|
||||
case <-exit:
|
||||
return
|
||||
// check the error and backoff
|
||||
default:
|
||||
if err != nil {
|
||||
s.opts.Logger.Logf(log.ErrorLevel, "Accept error: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// registrar is responsible for keeping the service registered to the registry.
|
||||
func (s *rpcServer) registrar(listener transport.Listener, addr string, config Options, exit chan bool) {
|
||||
logger := config.Logger
|
||||
|
||||
// Only process if it exists
|
||||
ticker := new(time.Ticker)
|
||||
if s.opts.RegisterInterval > time.Duration(0) {
|
||||
ticker = time.NewTicker(s.opts.RegisterInterval)
|
||||
}
|
||||
|
||||
// Return error chan
|
||||
var ch chan error
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
// Register self on interval
|
||||
case <-ticker.C:
|
||||
registered := s.isRegistered()
|
||||
|
||||
rerr := s.opts.RegisterCheck(s.opts.Context)
|
||||
if rerr != nil && registered {
|
||||
logger.Logf(log.ErrorLevel,
|
||||
"Server %s-%s register check error: %s, deregister it",
|
||||
config.Name,
|
||||
config.Id,
|
||||
rerr)
|
||||
// deregister self in case of error
|
||||
if err := s.Deregister(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s deregister error: %s", config.Name, config.Id, err)
|
||||
}
|
||||
} else if rerr != nil && !registered {
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s register check error: %s", config.Name, config.Id, rerr)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.Register(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s register error: %s", config.Name, config.Id, err)
|
||||
}
|
||||
|
||||
// Wait for exit signal
|
||||
case ch = <-s.exit:
|
||||
ticker.Stop()
|
||||
close(exit)
|
||||
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
// Shutting down, deregister
|
||||
if s.isRegistered() {
|
||||
if err := s.Deregister(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Server %s-%s deregister error: %s", config.Name, config.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for requests to finish
|
||||
if swg := s.getWg(); swg != nil {
|
||||
swg.Wait()
|
||||
}
|
||||
|
||||
// Close transport listener
|
||||
ch <- listener.Close()
|
||||
|
||||
brokerName := config.Broker.String()
|
||||
logger.Logf(log.InfoLevel, "Broker [%s] Disconnected from %s", brokerName, config.Broker.Address())
|
||||
|
||||
// Disconnect the broker
|
||||
if err := config.Broker.Disconnect(); err != nil {
|
||||
logger.Logf(log.ErrorLevel, "Broker [%s] Disconnect error: %v", brokerName, err)
|
||||
}
|
||||
|
||||
// Swap back address
|
||||
s.setOptsAddr(addr)
|
||||
}
|
||||
|
||||
func (s *rpcServer) serveReq(ctx context.Context,
|
||||
msg transport.Message,
|
||||
req *rpcRequest,
|
||||
resp *rpcResponse,
|
||||
rcodec codec.Codec) {
|
||||
logger := s.opts.Logger
|
||||
router := s.getRouter()
|
||||
|
||||
// serve the actual request using the request router
|
||||
if serveRequestError := router.ServeRequest(ctx, req, resp); serveRequestError != nil {
|
||||
// write an error response
|
||||
writeError := rcodec.Write(&codec.Message{
|
||||
Header: msg.Header,
|
||||
Error: serveRequestError.Error(),
|
||||
Type: codec.Error,
|
||||
}, nil)
|
||||
|
||||
// if the server request is an EOS error we let the socket know
|
||||
// sometimes the socket is already closed on the other side, so we can ignore that error
|
||||
alreadyClosed := errors.Is(serveRequestError, errLastStreamResponse) && errors.Is(writeError, io.EOF)
|
||||
|
||||
// could not write error response
|
||||
if writeError != nil && !alreadyClosed {
|
||||
logger.Logf(log.DebugLevel, "rpc: unable to write error response: %v", writeError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcServer) deferer(pool *socket.Pool, psock *socket.Socket, wg *waitGroup) {
|
||||
pool.Release(psock)
|
||||
wg.Done()
|
||||
|
||||
logger := s.opts.Logger
|
||||
if r := recover(); r != nil {
|
||||
logger.Log(log.ErrorLevel, "panic recovered: ", r)
|
||||
logger.Log(log.ErrorLevel, string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcServer) getRouter() Router {
|
||||
router := Router(s.router)
|
||||
|
||||
// if not nil use the router specified
|
||||
if s.opts.Router != nil {
|
||||
// create a wrapped function
|
||||
handler := func(ctx context.Context, req Request, rsp interface{}) error {
|
||||
return s.opts.Router.ServeRequest(ctx, req, rsp.(Response))
|
||||
}
|
||||
|
||||
// execute the wrapper for it
|
||||
for i := len(s.opts.HdlrWrappers); i > 0; i-- {
|
||||
handler = s.opts.HdlrWrappers[i-1](handler)
|
||||
}
|
||||
|
||||
// set the router
|
||||
router = rpcRouter{h: handler}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
)
|
||||
|
||||
// Implements the Streamer interface.
|
||||
type rpcStream struct {
|
||||
err error
|
||||
request Request
|
||||
codec codec.Codec
|
||||
context context.Context
|
||||
id string
|
||||
sync.RWMutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (r *rpcStream) Context() context.Context {
|
||||
return r.context
|
||||
}
|
||||
|
||||
func (r *rpcStream) Request() Request {
|
||||
return r.request
|
||||
}
|
||||
|
||||
func (r *rpcStream) Send(msg interface{}) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
resp := codec.Message{
|
||||
Target: r.request.Service(),
|
||||
Method: r.request.Method(),
|
||||
Endpoint: r.request.Endpoint(),
|
||||
Id: r.id,
|
||||
Type: codec.Response,
|
||||
}
|
||||
|
||||
if err := r.codec.Write(&resp, msg); err != nil {
|
||||
r.err = err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcStream) Recv(msg interface{}) error {
|
||||
req := new(codec.Message)
|
||||
req.Type = codec.Request
|
||||
|
||||
err := r.codec.ReadHeader(req, req.Type)
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if err != nil {
|
||||
// discard body
|
||||
_ = r.codec.ReadBody(nil)
|
||||
r.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
// check the error
|
||||
if len(req.Error) > 0 {
|
||||
// Check the client closed the stream
|
||||
switch req.Error {
|
||||
case errLastStreamResponse.Error():
|
||||
// discard body
|
||||
r.Unlock()
|
||||
_ = r.codec.ReadBody(nil)
|
||||
r.Lock()
|
||||
r.err = io.EOF
|
||||
return io.EOF
|
||||
default:
|
||||
return errors.New(req.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// we need to stay up to date with sequence numbers
|
||||
r.id = req.Id
|
||||
r.Unlock()
|
||||
err = r.codec.ReadBody(msg)
|
||||
r.Lock()
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rpcStream) Error() error {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *rpcStream) Close() error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.closed = true
|
||||
return r.codec.Close()
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"go-micro.dev/v6/codec/json"
|
||||
protoCodec "go-micro.dev/v6/codec/proto"
|
||||
)
|
||||
|
||||
// protoStruct implements proto.Message.
|
||||
type protoStruct struct {
|
||||
Payload string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"`
|
||||
}
|
||||
|
||||
func (m *protoStruct) Reset() { *m = protoStruct{} }
|
||||
func (m *protoStruct) String() string { return proto.CompactTextString(m) }
|
||||
func (*protoStruct) ProtoMessage() {}
|
||||
|
||||
// safeBuffer throws away everything and wont Read data back.
|
||||
type safeBuffer struct {
|
||||
sync.RWMutex
|
||||
buf []byte
|
||||
off int
|
||||
}
|
||||
|
||||
func (b *safeBuffer) Write(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// Cannot retain p, so we must copy it:
|
||||
p2 := make([]byte, len(p))
|
||||
copy(p2, p)
|
||||
b.Lock()
|
||||
b.buf = append(b.buf, p2...)
|
||||
b.Unlock()
|
||||
return len(p2), nil
|
||||
}
|
||||
|
||||
func (b *safeBuffer) Read(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
b.RLock()
|
||||
n = copy(p, b.buf[b.off:])
|
||||
b.RUnlock()
|
||||
if n == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
b.off += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (b *safeBuffer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRPCStream_Sequence(t *testing.T) {
|
||||
buffer := new(bytes.Buffer)
|
||||
rwc := readWriteCloser{
|
||||
rbuf: buffer,
|
||||
wbuf: buffer,
|
||||
}
|
||||
codec := json.NewCodec(&rwc)
|
||||
streamServer := rpcStream{
|
||||
codec: codec,
|
||||
request: &rpcRequest{
|
||||
codec: codec,
|
||||
},
|
||||
}
|
||||
|
||||
// Check if sequence is correct
|
||||
for i := 0; i < 1000; i++ {
|
||||
if err := streamServer.Send(fmt.Sprintf(`{"test":"value %d"}`, i)); err != nil {
|
||||
t.Errorf("Unexpected Send error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
var msg string
|
||||
if err := streamServer.Recv(&msg); err != nil {
|
||||
t.Errorf("Unexpected Recv error: %s", err)
|
||||
}
|
||||
if msg != fmt.Sprintf(`{"test":"value %d"}`, i) {
|
||||
t.Errorf("Unexpected msg: %s", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPCStream_Concurrency(t *testing.T) {
|
||||
buffer := new(safeBuffer)
|
||||
codec := protoCodec.NewCodec(buffer)
|
||||
streamServer := rpcStream{
|
||||
codec: codec,
|
||||
request: &rpcRequest{
|
||||
codec: codec,
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// Check if race conditions happen
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 50; i++ {
|
||||
msg := protoStruct{Payload: "test"}
|
||||
<-time.After(time.Duration(rand.Intn(50)) * time.Millisecond)
|
||||
if err := streamServer.Send(msg); err != nil {
|
||||
t.Errorf("Unexpected Send error: %s", err)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 50; i++ {
|
||||
<-time.After(time.Duration(rand.Intn(50)) * time.Millisecond)
|
||||
if err := streamServer.Recv(&protoStruct{}); err != nil {
|
||||
t.Errorf("Unexpected Recv error: %s", err)
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// waitgroup for global management of connections.
|
||||
type waitGroup struct {
|
||||
// global waitgroup
|
||||
gg *sync.WaitGroup
|
||||
// local waitgroup
|
||||
lg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewWaitGroup returns a new double waitgroup for global management of processes.
|
||||
func NewWaitGroup(gWg *sync.WaitGroup) *waitGroup {
|
||||
return &waitGroup{
|
||||
gg: gWg,
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// Package server is an interface for a micro server
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
signalutil "go-micro.dev/v6/internal/util/signal"
|
||||
log "go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
// Server is a simple micro server abstraction.
|
||||
type Server interface {
|
||||
// Initialize options
|
||||
Init(...Option) error
|
||||
// Retrieve the options
|
||||
Options() Options
|
||||
// Register a handler
|
||||
Handle(Handler) error
|
||||
// Create a new handler
|
||||
NewHandler(interface{}, ...HandlerOption) Handler
|
||||
// Create a new subscriber
|
||||
NewSubscriber(string, interface{}, ...SubscriberOption) Subscriber
|
||||
// Register a subscriber
|
||||
Subscribe(Subscriber) error
|
||||
// Start the server
|
||||
Start() error
|
||||
// Stop the server
|
||||
Stop() error
|
||||
// Server implementation
|
||||
String() string
|
||||
}
|
||||
|
||||
// Router handle serving messages.
|
||||
type Router interface {
|
||||
// ProcessMessage processes a message
|
||||
ProcessMessage(context.Context, string, 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.
|
||||
type Request interface {
|
||||
// Service name requested
|
||||
Service() string
|
||||
// The action requested
|
||||
Method() string
|
||||
// Endpoint name requested
|
||||
Endpoint() string
|
||||
// Content type provided
|
||||
ContentType() string
|
||||
// Header of the request
|
||||
Header() map[string]string
|
||||
// Body is the initial decoded value
|
||||
Body() interface{}
|
||||
// Read the undecoded request body
|
||||
Read() ([]byte, error)
|
||||
// The encoded message stream
|
||||
Codec() codec.Reader
|
||||
// Indicates whether its a stream
|
||||
Stream() bool
|
||||
}
|
||||
|
||||
// Response is the response writer for unencoded messages.
|
||||
type Response interface {
|
||||
// Encoded writer
|
||||
Codec() codec.Writer
|
||||
// Write the header
|
||||
WriteHeader(map[string]string)
|
||||
// write a response directly to the client
|
||||
Write([]byte) error
|
||||
}
|
||||
|
||||
// Stream represents a stream established with a client.
|
||||
// A stream can be bidirectional which is indicated by the request.
|
||||
// The last error will be left in Error().
|
||||
// EOF indicates end of the stream.
|
||||
type Stream interface {
|
||||
Context() context.Context
|
||||
Request() Request
|
||||
Send(interface{}) error
|
||||
Recv(interface{}) error
|
||||
Error() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Handler interface represents a request handler. It's generated
|
||||
// by passing any type of public concrete object with endpoints into server.NewHandler.
|
||||
// Most will pass in a struct.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Greeter struct {}
|
||||
//
|
||||
// func (g *Greeter) Hello(context, request, response) error {
|
||||
// return nil
|
||||
// }
|
||||
type Handler interface {
|
||||
Name() string
|
||||
Handler() interface{}
|
||||
Endpoints() []*registry.Endpoint
|
||||
Options() HandlerOptions
|
||||
}
|
||||
|
||||
// Subscriber interface represents a subscription to a given topic using
|
||||
// a specific subscriber function or object with endpoints. It mirrors
|
||||
// the handler in its behavior.
|
||||
type Subscriber interface {
|
||||
Topic() string
|
||||
Subscriber() interface{}
|
||||
Endpoints() []*registry.Endpoint
|
||||
Options() SubscriberOptions
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
var (
|
||||
DefaultAddress = ":0"
|
||||
DefaultName = "go.micro.server"
|
||||
DefaultVersion = "latest"
|
||||
DefaultId = uuid.New().String()
|
||||
DefaultServer Server = NewRPCServer()
|
||||
DefaultRouter = newRpcRouter()
|
||||
DefaultRegisterCheck = func(context.Context) error { return nil }
|
||||
DefaultRegisterInterval = time.Second * 30
|
||||
DefaultRegisterTTL = time.Second * 90
|
||||
|
||||
// NewServer creates a new server.
|
||||
NewServer func(...Option) Server = NewRPCServer
|
||||
)
|
||||
|
||||
// DefaultOptions returns config options for the default service.
|
||||
func DefaultOptions() Options {
|
||||
return DefaultServer.Options()
|
||||
}
|
||||
|
||||
func Init(opt ...Option) {
|
||||
if DefaultServer == nil {
|
||||
DefaultServer = NewRPCServer(opt...)
|
||||
}
|
||||
_ = DefaultServer.Init(opt...)
|
||||
}
|
||||
|
||||
// NewRouter returns a new router.
|
||||
func NewRouter() *router {
|
||||
return newRpcRouter()
|
||||
}
|
||||
|
||||
// NewSubscriber creates a new subscriber interface with the given topic
|
||||
// and handler using the default server.
|
||||
func NewSubscriber(topic string, h interface{}, opts ...SubscriberOption) Subscriber {
|
||||
return DefaultServer.NewSubscriber(topic, h, opts...)
|
||||
}
|
||||
|
||||
// NewHandler creates a new handler interface using the default server
|
||||
// Handlers are required to be a public object with public
|
||||
// endpoints. Call to a service endpoint such as Foo.Bar expects
|
||||
// the type:
|
||||
//
|
||||
// type Foo struct {}
|
||||
// func (f *Foo) Bar(ctx, req, rsp) error {
|
||||
// return nil
|
||||
// }
|
||||
func NewHandler(h interface{}, opts ...HandlerOption) Handler {
|
||||
return DefaultServer.NewHandler(h, opts...)
|
||||
}
|
||||
|
||||
// Handle registers a handler interface with the default server to
|
||||
// handle inbound requests.
|
||||
func Handle(h Handler) error {
|
||||
return DefaultServer.Handle(h)
|
||||
}
|
||||
|
||||
// Subscribe registers a subscriber interface with the default server
|
||||
// which subscribes to specified topic with the broker.
|
||||
func Subscribe(s Subscriber) error {
|
||||
return DefaultServer.Subscribe(s)
|
||||
}
|
||||
|
||||
// Run starts the default server and waits for a kill
|
||||
// signal before exiting. Also registers/deregisters the server.
|
||||
func Run() error {
|
||||
if err := Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, signalutil.Shutdown()...)
|
||||
DefaultServer.Options().Logger.Logf(log.InfoLevel, "Received signal %s", <-ch)
|
||||
|
||||
return Stop()
|
||||
}
|
||||
|
||||
// Start starts the default server.
|
||||
func Start() error {
|
||||
config := DefaultServer.Options()
|
||||
config.Logger.Logf(log.InfoLevel, "Starting server %s id %s", config.Name, config.Id)
|
||||
return DefaultServer.Start()
|
||||
}
|
||||
|
||||
// Stop stops the default server.
|
||||
func Stop() error {
|
||||
DefaultServer.Options().Logger.Logf(log.InfoLevel, "Stopping server")
|
||||
return DefaultServer.Stop()
|
||||
}
|
||||
|
||||
// String returns name of Server implementation.
|
||||
func String() string {
|
||||
return DefaultServer.String()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
subSig = "func(context.Context, interface{}) error"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
reqType reflect.Type
|
||||
ctxType reflect.Type
|
||||
method reflect.Value
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
opts SubscriberOptions
|
||||
typ reflect.Type
|
||||
subscriber interface{}
|
||||
rcvr reflect.Value
|
||||
topic string
|
||||
handlers []*handler
|
||||
endpoints []*registry.Endpoint
|
||||
}
|
||||
|
||||
func newSubscriber(topic string, sub interface{}, opts ...SubscriberOption) Subscriber {
|
||||
options := SubscriberOptions{
|
||||
AutoAck: true,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var endpoints []*registry.Endpoint
|
||||
var handlers []*handler
|
||||
|
||||
if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func {
|
||||
h := &handler{
|
||||
method: reflect.ValueOf(sub),
|
||||
}
|
||||
|
||||
switch typ.NumIn() {
|
||||
case 1:
|
||||
h.reqType = typ.In(0)
|
||||
case 2:
|
||||
h.ctxType = typ.In(0)
|
||||
h.reqType = typ.In(1)
|
||||
}
|
||||
|
||||
handlers = append(handlers, h)
|
||||
|
||||
endpoints = append(endpoints, ®istry.Endpoint{
|
||||
Name: "Func",
|
||||
Request: extractSubValue(typ),
|
||||
Metadata: map[string]string{
|
||||
"topic": topic,
|
||||
"subscriber": "true",
|
||||
},
|
||||
})
|
||||
} else {
|
||||
hdlr := reflect.ValueOf(sub)
|
||||
name := reflect.Indirect(hdlr).Type().Name()
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
method := typ.Method(m)
|
||||
h := &handler{
|
||||
method: method.Func,
|
||||
}
|
||||
|
||||
switch method.Type.NumIn() {
|
||||
case 2:
|
||||
h.reqType = method.Type.In(1)
|
||||
case 3:
|
||||
h.ctxType = method.Type.In(1)
|
||||
h.reqType = method.Type.In(2)
|
||||
}
|
||||
|
||||
handlers = append(handlers, h)
|
||||
|
||||
endpoints = append(endpoints, ®istry.Endpoint{
|
||||
Name: name + "." + method.Name,
|
||||
Request: extractSubValue(method.Type),
|
||||
Metadata: map[string]string{
|
||||
"topic": topic,
|
||||
"subscriber": "true",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &subscriber{
|
||||
rcvr: reflect.ValueOf(sub),
|
||||
typ: reflect.TypeOf(sub),
|
||||
topic: topic,
|
||||
subscriber: sub,
|
||||
handlers: handlers,
|
||||
endpoints: endpoints,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func validateSubscriber(sub Subscriber) error {
|
||||
typ := reflect.TypeOf(sub.Subscriber())
|
||||
var argType reflect.Type
|
||||
|
||||
if typ.Kind() == reflect.Func {
|
||||
name := "Func"
|
||||
switch typ.NumIn() {
|
||||
case 2:
|
||||
argType = typ.In(1)
|
||||
default:
|
||||
return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s", name, typ.NumIn(), subSig)
|
||||
}
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
|
||||
}
|
||||
if typ.NumOut() != 1 {
|
||||
return fmt.Errorf("subscriber %v has wrong number of outs: %v require signature %s",
|
||||
name, typ.NumOut(), subSig)
|
||||
}
|
||||
if returnType := typ.Out(0); returnType != typeOfError {
|
||||
return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
|
||||
}
|
||||
} else {
|
||||
hdlr := reflect.ValueOf(sub.Subscriber())
|
||||
name := reflect.Indirect(hdlr).Type().Name()
|
||||
|
||||
for m := 0; m < typ.NumMethod(); m++ {
|
||||
method := typ.Method(m)
|
||||
|
||||
switch method.Type.NumIn() {
|
||||
case 3:
|
||||
argType = method.Type.In(2)
|
||||
default:
|
||||
return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s",
|
||||
name, method.Name, method.Type.NumIn(), subSig)
|
||||
}
|
||||
|
||||
if !isExportedOrBuiltinType(argType) {
|
||||
return fmt.Errorf("%v argument type not exported: %v", name, argType)
|
||||
}
|
||||
if method.Type.NumOut() != 1 {
|
||||
return fmt.Errorf(
|
||||
"subscriber %v.%v has wrong number of outs: %v require signature %s",
|
||||
name, method.Name, method.Type.NumOut(), subSig)
|
||||
}
|
||||
if returnType := method.Type.Out(0); returnType != typeOfError {
|
||||
return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriber) Topic() string {
|
||||
return s.topic
|
||||
}
|
||||
|
||||
func (s *subscriber) Subscriber() interface{} {
|
||||
return s.subscriber
|
||||
}
|
||||
|
||||
func (s *subscriber) Endpoints() []*registry.Endpoint {
|
||||
return s.endpoints
|
||||
}
|
||||
|
||||
func (s *subscriber) Options() SubscriberOptions {
|
||||
return s.opts
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// HandlerFunc represents a single method of a handler. It's used primarily
|
||||
// for the wrappers. What's handed to the actual method is the concrete
|
||||
// request and response types.
|
||||
type HandlerFunc func(ctx context.Context, req Request, rsp interface{}) error
|
||||
|
||||
// SubscriberFunc represents a single method of a subscriber. It's used primarily
|
||||
// for the wrappers. What's handed to the actual method is the concrete
|
||||
// publication message.
|
||||
type SubscriberFunc func(ctx context.Context, msg Message) error
|
||||
|
||||
// HandlerWrapper wraps the HandlerFunc and returns the equivalent.
|
||||
type HandlerWrapper func(HandlerFunc) HandlerFunc
|
||||
|
||||
// SubscriberWrapper wraps the SubscriberFunc and returns the equivalent.
|
||||
type SubscriberWrapper func(SubscriberFunc) SubscriberFunc
|
||||
|
||||
// StreamWrapper wraps a Stream interface and returns the equivalent.
|
||||
// Because streams exist for the lifetime of a method invocation this
|
||||
// is a convenient way to wrap a Stream as its in use for trace, monitoring,
|
||||
// metrics, etc.
|
||||
type StreamWrapper func(Stream) Stream
|
||||
Reference in New Issue
Block a user