move implementations to external repos (#17)
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
@@ -1,247 +0,0 @@
|
||||
// Package grpc is a grpc proxy built for the go-micro/server
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/unistack-org/micro/v3/client"
|
||||
grpcc "github.com/unistack-org/micro/v3/client/grpc"
|
||||
"github.com/unistack-org/micro/v3/codec"
|
||||
"github.com/unistack-org/micro/v3/codec/bytes"
|
||||
"github.com/unistack-org/micro/v3/errors"
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/unistack-org/micro/v3/proxy"
|
||||
"github.com/unistack-org/micro/v3/server"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Proxy will transparently proxy requests to an endpoint.
|
||||
// If no endpoint is specified it will call a service using the client.
|
||||
type Proxy struct {
|
||||
// embed options
|
||||
options proxy.Options
|
||||
|
||||
// The client to use for outbound requests in the local network
|
||||
Client client.Client
|
||||
|
||||
// Endpoint to route all calls to
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
// read client request and write to server
|
||||
func readLoop(r server.Request, s client.Stream) error {
|
||||
// request to backend server
|
||||
req := s.Request()
|
||||
|
||||
for {
|
||||
// get data from client
|
||||
// no need to decode it
|
||||
body, err := r.Read()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the header from client
|
||||
hdr := r.Header()
|
||||
msg := &codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: hdr,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
// write the raw request
|
||||
err = req.Codec().Write(msg, nil)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessMessage acts as a message exchange and forwards messages to ongoing topics
|
||||
// TODO: should we look at p.Endpoint and only send to the local endpoint? probably
|
||||
func (p *Proxy) ProcessMessage(ctx context.Context, msg server.Message) error {
|
||||
// TODO: check that we're not broadcast storming by sending to the same topic
|
||||
// that we're actually subscribed to
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy received message for %s", msg.Topic())
|
||||
}
|
||||
|
||||
// directly publish to the local client
|
||||
return p.Client.Publish(ctx, msg)
|
||||
}
|
||||
|
||||
// ServeRequest honours the server.Router interface
|
||||
func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error {
|
||||
// service name to call
|
||||
service := req.Service()
|
||||
// endpoint to call
|
||||
endpoint := req.Endpoint()
|
||||
|
||||
if len(service) == 0 {
|
||||
return errors.BadRequest("go.micro.proxy", "service name is blank")
|
||||
}
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy received request for %s %s", service, endpoint)
|
||||
}
|
||||
|
||||
// no retries with the proxy
|
||||
opts := []client.CallOption{
|
||||
client.WithRetries(0),
|
||||
}
|
||||
|
||||
// call a specific backend
|
||||
if len(p.Endpoint) > 0 {
|
||||
// address:port
|
||||
if parts := strings.Split(p.Endpoint, ":"); len(parts) > 1 {
|
||||
opts = append(opts, client.WithAddress(p.Endpoint))
|
||||
// use as service name
|
||||
} else {
|
||||
service = p.Endpoint
|
||||
}
|
||||
}
|
||||
|
||||
// serve the normal way
|
||||
return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp, opts...)
|
||||
}
|
||||
|
||||
func (p *Proxy) serveRequest(ctx context.Context, link client.Client, service, endpoint string, req server.Request, rsp server.Response, opts ...client.CallOption) error {
|
||||
// read initial request
|
||||
body, err := req.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new request with raw bytes body
|
||||
creq := link.NewRequest(service, endpoint, &bytes.Frame{Data: body}, client.WithContentType(req.ContentType()))
|
||||
|
||||
// not a stream so make a client.Call request
|
||||
if !req.Stream() {
|
||||
crsp := new(bytes.Frame)
|
||||
|
||||
// make a call to the backend
|
||||
if err := link.Call(ctx, creq, crsp, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write the response
|
||||
if err := rsp.Write(crsp.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// new context with cancel
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// create new stream
|
||||
stream, err := link.Stream(ctx, creq, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
// with a grpc stream we have to refire the initial request
|
||||
// client request to start the server side
|
||||
|
||||
// get the header from client
|
||||
msg := &codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: req.Header(),
|
||||
Body: body,
|
||||
}
|
||||
|
||||
// write the raw request
|
||||
err = stream.Request().Codec().Write(msg, nil)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create client request read loop if streaming
|
||||
go func() {
|
||||
err := readLoop(req, stream)
|
||||
if err != nil && err != io.EOF {
|
||||
// cancel the context
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
// get raw response
|
||||
resp := stream.Response()
|
||||
|
||||
// create server response write loop
|
||||
for {
|
||||
// read backend response body
|
||||
body, err := resp.Read()
|
||||
if err != nil {
|
||||
// when we're done if its a grpc stream we have to set the trailer
|
||||
if cc, ok := stream.(grpc.ClientStream); ok {
|
||||
if ss, ok := resp.Codec().(grpc.ServerStream); ok {
|
||||
ss.SetTrailer(cc.Trailer())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read backend response header
|
||||
hdr := resp.Header()
|
||||
|
||||
// write raw response header to client
|
||||
rsp.WriteHeader(hdr)
|
||||
|
||||
// write raw response body to client
|
||||
err = rsp.Write(body)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) String() string {
|
||||
return "grpc"
|
||||
}
|
||||
|
||||
// NewProxy returns a new proxy which will route based on mucp headers
|
||||
func NewProxy(opts ...proxy.Option) proxy.Proxy {
|
||||
var options proxy.Options
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// create a new grpc proxy
|
||||
p := new(Proxy)
|
||||
p.options = options
|
||||
|
||||
// set the client
|
||||
p.Client = options.Client
|
||||
// set the endpoint
|
||||
p.Endpoint = options.Endpoint
|
||||
|
||||
// set the default client
|
||||
if p.Client == nil {
|
||||
p.Client = grpcc.NewClient()
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
// Package http provides a micro rpc to http proxy
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/unistack-org/micro/v3/errors"
|
||||
"github.com/unistack-org/micro/v3/proxy"
|
||||
"github.com/unistack-org/micro/v3/server"
|
||||
)
|
||||
|
||||
// Proxy will proxy rpc requests as http POST requests. It is a server.Proxy
|
||||
type Proxy struct {
|
||||
options proxy.Options
|
||||
|
||||
// The http backend to call
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func getMethod(hdr map[string]string) string {
|
||||
switch hdr["Micro-Method"] {
|
||||
case "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH":
|
||||
return hdr["Micro-Method"]
|
||||
default:
|
||||
return "POST"
|
||||
}
|
||||
}
|
||||
|
||||
func getEndpoint(hdr map[string]string) string {
|
||||
ep := hdr["Micro-Endpoint"]
|
||||
if len(ep) > 0 && ep[0] == '/' {
|
||||
return ep
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getTopic(hdr map[string]string) string {
|
||||
ep := hdr["Micro-Topic"]
|
||||
if len(ep) > 0 && ep[0] == '/' {
|
||||
return ep
|
||||
}
|
||||
return "/" + hdr["Micro-Topic"]
|
||||
}
|
||||
|
||||
// ProcessMessage handles incoming asynchronous messages
|
||||
func (p *Proxy) ProcessMessage(ctx context.Context, msg server.Message) error {
|
||||
if p.Endpoint == "" {
|
||||
p.Endpoint = proxy.DefaultEndpoint
|
||||
}
|
||||
|
||||
// get the header
|
||||
hdr := msg.Header()
|
||||
|
||||
// get topic
|
||||
// use /topic as endpoint
|
||||
endpoint := getTopic(hdr)
|
||||
|
||||
// set the endpoint
|
||||
if len(endpoint) == 0 {
|
||||
endpoint = p.Endpoint
|
||||
} else {
|
||||
// add endpoint to backend
|
||||
u, err := url.Parse(p.Endpoint)
|
||||
if err != nil {
|
||||
return errors.InternalServerError(msg.Topic(), err.Error())
|
||||
}
|
||||
u.Path = path.Join(u.Path, endpoint)
|
||||
endpoint = u.String()
|
||||
}
|
||||
|
||||
// send to backend
|
||||
hreq, err := http.NewRequest("POST", endpoint, bytes.NewReader(msg.Body()))
|
||||
if err != nil {
|
||||
return errors.InternalServerError(msg.Topic(), err.Error())
|
||||
}
|
||||
|
||||
// set the headers
|
||||
for k, v := range hdr {
|
||||
hreq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
// make the call
|
||||
hrsp, err := http.DefaultClient.Do(hreq)
|
||||
if err != nil {
|
||||
return errors.InternalServerError(msg.Topic(), err.Error())
|
||||
}
|
||||
|
||||
// read body
|
||||
b, err := ioutil.ReadAll(hrsp.Body)
|
||||
hrsp.Body.Close()
|
||||
if err != nil {
|
||||
return errors.InternalServerError(msg.Topic(), err.Error())
|
||||
}
|
||||
|
||||
if hrsp.StatusCode != 200 {
|
||||
return errors.New(msg.Topic(), string(b), int32(hrsp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeRequest honours the server.Router interface
|
||||
func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error {
|
||||
if p.Endpoint == "" {
|
||||
p.Endpoint = proxy.DefaultEndpoint
|
||||
}
|
||||
|
||||
for {
|
||||
// get data
|
||||
body, err := req.Read()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the header
|
||||
hdr := req.Header()
|
||||
|
||||
// get method
|
||||
method := getMethod(hdr)
|
||||
|
||||
// get endpoint
|
||||
endpoint := getEndpoint(hdr)
|
||||
|
||||
// set the endpoint
|
||||
if len(endpoint) == 0 {
|
||||
endpoint = p.Endpoint
|
||||
} else {
|
||||
// add endpoint to backend
|
||||
u, err := url.Parse(p.Endpoint)
|
||||
if err != nil {
|
||||
return errors.InternalServerError(req.Service(), err.Error())
|
||||
}
|
||||
u.Path = path.Join(u.Path, endpoint)
|
||||
endpoint = u.String()
|
||||
}
|
||||
|
||||
// send to backend
|
||||
hreq, err := http.NewRequest(method, endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return errors.InternalServerError(req.Service(), err.Error())
|
||||
}
|
||||
|
||||
// set the headers
|
||||
for k, v := range hdr {
|
||||
hreq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
// make the call
|
||||
hrsp, err := http.DefaultClient.Do(hreq)
|
||||
if err != nil {
|
||||
return errors.InternalServerError(req.Service(), err.Error())
|
||||
}
|
||||
|
||||
// read body
|
||||
b, err := ioutil.ReadAll(hrsp.Body)
|
||||
hrsp.Body.Close()
|
||||
if err != nil {
|
||||
return errors.InternalServerError(req.Service(), err.Error())
|
||||
}
|
||||
|
||||
// set response headers
|
||||
hdr = map[string]string{}
|
||||
for k := range hrsp.Header {
|
||||
hdr[k] = hrsp.Header.Get(k)
|
||||
}
|
||||
// write the header
|
||||
rsp.WriteHeader(hdr)
|
||||
// write the body
|
||||
err = rsp.Write(b)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.InternalServerError(req.Service(), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) String() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
// NewSingleHostProxy returns a router which sends requests to a single http backend
|
||||
func NewSingleHostProxy(url string) proxy.Proxy {
|
||||
return &Proxy{
|
||||
Endpoint: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewProxy returns a new proxy which will route using a http client
|
||||
func NewProxy(opts ...proxy.Option) proxy.Proxy {
|
||||
var options proxy.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
p := new(Proxy)
|
||||
p.Endpoint = options.Endpoint
|
||||
p.options = options
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/unistack-org/micro/v3/client"
|
||||
cmucp "github.com/unistack-org/micro/v3/client/mucp"
|
||||
"github.com/unistack-org/micro/v3/registry/memory"
|
||||
"github.com/unistack-org/micro/v3/router"
|
||||
"github.com/unistack-org/micro/v3/router/registry"
|
||||
"github.com/unistack-org/micro/v3/server"
|
||||
"github.com/unistack-org/micro/v3/server/mucp"
|
||||
)
|
||||
|
||||
type testHandler struct{}
|
||||
|
||||
func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"hello": "world"}`))
|
||||
}
|
||||
|
||||
func TestHTTPProxy(t *testing.T) {
|
||||
c, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
addr := c.Addr().String()
|
||||
|
||||
url := fmt.Sprintf("http://%s", addr)
|
||||
|
||||
testCases := []struct {
|
||||
// http endpoint to call e.g /foo/bar
|
||||
httpEp string
|
||||
// rpc endpoint called e.g Foo.Bar
|
||||
rpcEp string
|
||||
// should be an error
|
||||
err bool
|
||||
}{
|
||||
{"/", "Foo.Bar", false},
|
||||
{"/", "Foo.Baz", false},
|
||||
{"/helloworld", "Hello.World", true},
|
||||
}
|
||||
|
||||
// handler
|
||||
http.Handle("/", new(testHandler))
|
||||
|
||||
// new proxy
|
||||
p := NewSingleHostProxy(url)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
reg := memory.NewRegistry()
|
||||
rtr := registry.NewRouter(
|
||||
router.Registry(reg),
|
||||
)
|
||||
|
||||
// new micro service
|
||||
service := mucp.NewServer(
|
||||
server.Context(ctx),
|
||||
server.Name("foobar"),
|
||||
server.Registry(reg),
|
||||
server.WithRouter(p),
|
||||
)
|
||||
|
||||
service.Start()
|
||||
defer service.Stop()
|
||||
|
||||
// run service
|
||||
// server
|
||||
go http.Serve(c, nil)
|
||||
|
||||
cl := cmucp.NewClient(
|
||||
client.Router(rtr),
|
||||
)
|
||||
|
||||
for _, test := range testCases {
|
||||
req := cl.NewRequest("foobar", test.rpcEp, map[string]string{"foo": "bar"}, client.WithContentType("application/json"))
|
||||
var rsp map[string]string
|
||||
err := cl.Call(ctx, req, &rsp)
|
||||
if err != nil && test.err == false {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := rsp["hello"]; v != "world" {
|
||||
t.Fatalf("Expected hello world got %s from %s", v, test.rpcEp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,676 +0,0 @@
|
||||
// Package mucp transparently forwards the incoming request using a go-micro client.
|
||||
package mucp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/unistack-org/micro/v3/client"
|
||||
grpcc "github.com/unistack-org/micro/v3/client/grpc"
|
||||
"github.com/unistack-org/micro/v3/codec"
|
||||
"github.com/unistack-org/micro/v3/codec/bytes"
|
||||
"github.com/unistack-org/micro/v3/errors"
|
||||
"github.com/unistack-org/micro/v3/logger"
|
||||
"github.com/unistack-org/micro/v3/metadata"
|
||||
"github.com/unistack-org/micro/v3/proxy"
|
||||
"github.com/unistack-org/micro/v3/router"
|
||||
"github.com/unistack-org/micro/v3/router/registry"
|
||||
"github.com/unistack-org/micro/v3/selector"
|
||||
"github.com/unistack-org/micro/v3/selector/roundrobin"
|
||||
"github.com/unistack-org/micro/v3/server"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Proxy will transparently proxy requests to an endpoint.
|
||||
// If no endpoint is specified it will call a service using the client.
|
||||
type Proxy struct {
|
||||
// embed options
|
||||
options proxy.Options
|
||||
|
||||
// Endpoint specifies the fixed service endpoint to call.
|
||||
Endpoint string
|
||||
|
||||
// The client to use for outbound requests in the local network
|
||||
Client client.Client
|
||||
|
||||
// Links are used for outbound requests not in the local network
|
||||
Links map[string]client.Client
|
||||
|
||||
// The router for routes
|
||||
Router router.Router
|
||||
|
||||
// A fib of routes service:address
|
||||
sync.RWMutex
|
||||
Routes map[string]map[uint64]router.Route
|
||||
|
||||
// selector used for load balancing
|
||||
Selector selector.Selector
|
||||
}
|
||||
|
||||
// read client request and write to server
|
||||
func readLoop(r server.Request, s client.Stream) error {
|
||||
// request to backend server
|
||||
req := s.Request()
|
||||
|
||||
for {
|
||||
// get data from client
|
||||
// no need to decode it
|
||||
body, err := r.Read()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the header from client
|
||||
hdr := r.Header()
|
||||
msg := &codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: hdr,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
// write the raw request
|
||||
err = req.Codec().Write(msg, nil)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toNodes returns a list of node addresses from given routes
|
||||
func toNodes(routes []router.Route) []string {
|
||||
nodes := make([]string, 0, len(routes))
|
||||
|
||||
for _, node := range routes {
|
||||
address := node.Address
|
||||
if len(node.Gateway) > 0 {
|
||||
address = node.Gateway
|
||||
}
|
||||
nodes = append(nodes, address)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func toSlice(r map[uint64]router.Route) []router.Route {
|
||||
routes := make([]router.Route, 0, len(r))
|
||||
|
||||
for _, v := range r {
|
||||
routes = append(routes, v)
|
||||
}
|
||||
|
||||
// sort the routes in order of metric
|
||||
sort.Slice(routes, func(i, j int) bool { return routes[i].Metric < routes[j].Metric })
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
func (p *Proxy) filterRoutes(ctx context.Context, routes []router.Route) []router.Route {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
return routes
|
||||
}
|
||||
|
||||
//nolint:prealloc
|
||||
var filteredRoutes []router.Route
|
||||
|
||||
// filter the routes based on our headers
|
||||
for _, route := range routes {
|
||||
// process only routes for this id
|
||||
if id, ok := md.Get("Micro-Router"); ok && len(id) > 0 {
|
||||
if route.Router != id {
|
||||
// skip routes that don't match
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// only process routes with this network
|
||||
if net, ok := md.Get("Micro-Namespace"); ok && len(net) > 0 {
|
||||
if route.Network != router.DefaultNetwork && route.Network != net {
|
||||
// skip routes that don't match
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// process only this gateway
|
||||
if gw, ok := md.Get("Micro-Gateway"); ok && len(gw) > 0 {
|
||||
// if the gateway matches our address
|
||||
// special case, take the routes with no gateway
|
||||
// TODO: should we strip the gateway from the context?
|
||||
if gw == p.Router.Options().Address {
|
||||
if len(route.Gateway) > 0 && route.Gateway != gw {
|
||||
continue
|
||||
}
|
||||
// otherwise its a local route and we're keeping it
|
||||
} else {
|
||||
// gateway does not match our own
|
||||
if route.Gateway != gw {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: address based filtering
|
||||
// address := md["Micro-Address"]
|
||||
|
||||
// TODO: label based filtering
|
||||
// requires new field in routing table : route.Labels
|
||||
|
||||
// passed the filter checks
|
||||
filteredRoutes = append(filteredRoutes, route)
|
||||
}
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy filtered routes %+v", filteredRoutes)
|
||||
}
|
||||
|
||||
return filteredRoutes
|
||||
}
|
||||
|
||||
func (p *Proxy) getLink(r router.Route) (client.Client, error) {
|
||||
if r.Link == "local" || len(p.Links) == 0 {
|
||||
return p.Client, nil
|
||||
}
|
||||
l, ok := p.Links[r.Link]
|
||||
if !ok {
|
||||
return nil, errors.InternalServerError("go.micro.proxy", "link not found")
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (p *Proxy) getRoute(ctx context.Context, service string) ([]router.Route, error) {
|
||||
// lookup the route cache first
|
||||
p.RLock()
|
||||
cached, ok := p.Routes[service]
|
||||
p.RUnlock()
|
||||
if ok {
|
||||
return p.filterRoutes(ctx, toSlice(cached)), nil
|
||||
}
|
||||
|
||||
// cache routes for the service
|
||||
routes, err := p.cacheRoutes(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.filterRoutes(ctx, routes), nil
|
||||
}
|
||||
|
||||
func (p *Proxy) cacheRoutes(service string) ([]router.Route, error) {
|
||||
// lookup the routes in the router
|
||||
results, err := p.Router.Lookup(router.QueryService(service), router.QueryNetwork("*"))
|
||||
if err != nil {
|
||||
// assumption that we're ok with stale routes
|
||||
logger.Debugf("Failed to lookup route for %s: %v", service, err)
|
||||
// otherwise return the error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update the proxy cache
|
||||
p.Lock()
|
||||
|
||||
// delete the existing reference to the service
|
||||
delete(p.Routes, service)
|
||||
|
||||
for _, route := range results {
|
||||
// create if does not exist
|
||||
if _, ok := p.Routes[service]; !ok {
|
||||
p.Routes[service] = make(map[uint64]router.Route)
|
||||
}
|
||||
// cache the route based on its unique hash
|
||||
p.Routes[service][route.Hash()] = route
|
||||
}
|
||||
|
||||
// make a copy of the service routes
|
||||
routes := p.Routes[service]
|
||||
|
||||
p.Unlock()
|
||||
|
||||
// return routes to the caller
|
||||
return toSlice(routes), nil
|
||||
}
|
||||
|
||||
// refreshMetrics will refresh any metrics for our local cached routes.
|
||||
// we may not receive new watch events for these as they change.
|
||||
func (p *Proxy) refreshMetrics() {
|
||||
// get a list of services to update
|
||||
p.RLock()
|
||||
|
||||
services := make([]string, 0, len(p.Routes))
|
||||
|
||||
for service := range p.Routes {
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
p.RUnlock()
|
||||
|
||||
// get and cache the routes for the service
|
||||
for _, service := range services {
|
||||
p.cacheRoutes(service)
|
||||
}
|
||||
}
|
||||
|
||||
// manageRoutes applies action on a given route to Proxy route cache
|
||||
func (p *Proxy) manageRoutes(route router.Route, action string) error {
|
||||
// we only cache what we are actually concerned with
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy taking route action %v %+v\n", action, route)
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "create", "update":
|
||||
if _, ok := p.Routes[route.Service]; !ok {
|
||||
return fmt.Errorf("not called %s", route.Service)
|
||||
}
|
||||
p.Routes[route.Service][route.Hash()] = route
|
||||
case "delete":
|
||||
// delete that specific route
|
||||
delete(p.Routes[route.Service], route.Hash())
|
||||
// clean up the cache entirely
|
||||
if len(p.Routes[route.Service]) == 0 {
|
||||
delete(p.Routes, route.Service)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown action: %s", action)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// watchRoutes watches service routes and updates proxy cache
|
||||
func (p *Proxy) watchRoutes() {
|
||||
// route watcher
|
||||
w, err := p.Router.Watch()
|
||||
if err != nil {
|
||||
logger.Debugf("Error watching router: %v", err)
|
||||
return
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
for {
|
||||
event, err := w.Next()
|
||||
if err != nil {
|
||||
logger.Debugf("Error watching router: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.manageRoutes(event.Route, event.Type.String()); err != nil {
|
||||
// TODO: should we bail here?
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessMessage acts as a message exchange and forwards messages to ongoing topics
|
||||
// TODO: should we look at p.Endpoint and only send to the local endpoint? probably
|
||||
func (p *Proxy) ProcessMessage(ctx context.Context, msg server.Message) error {
|
||||
// TODO: check that we're not broadcast storming by sending to the same topic
|
||||
// that we're actually subscribed to
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy received message for %s", msg.Topic())
|
||||
}
|
||||
|
||||
var errors []string
|
||||
|
||||
// directly publish to the local client
|
||||
if err := p.Client.Publish(ctx, msg); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
|
||||
// publish to all links
|
||||
for _, client := range p.Links {
|
||||
if err := client.Publish(ctx, msg); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// there is no error...muahaha
|
||||
return fmt.Errorf("Message processing error: %s", strings.Join(errors, "\n"))
|
||||
}
|
||||
|
||||
// ServeRequest honours the server.Router interface
|
||||
func (p *Proxy) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error {
|
||||
// determine if its local routing
|
||||
var local bool
|
||||
// address to call
|
||||
var addresses []string
|
||||
// routes
|
||||
var routes []router.Route
|
||||
// service name to call
|
||||
service := req.Service()
|
||||
// endpoint to call
|
||||
endpoint := req.Endpoint()
|
||||
|
||||
if len(service) == 0 {
|
||||
return errors.BadRequest("go.micro.proxy", "service name is blank")
|
||||
}
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy received request for %s %s", service, endpoint)
|
||||
}
|
||||
|
||||
// are we network routing or local routing
|
||||
if len(p.Links) == 0 {
|
||||
local = true
|
||||
}
|
||||
|
||||
// if there is a proxy being used, e.g. micro network, we don't need to
|
||||
// look up the routes since they'll be ignored by the client
|
||||
if len(p.Client.Options().Proxy) > 0 {
|
||||
return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp)
|
||||
}
|
||||
|
||||
// call a specific backend endpoint either by name or address
|
||||
if len(p.Endpoint) > 0 {
|
||||
// address:port
|
||||
if parts := strings.Split(p.Endpoint, ":"); len(parts) > 1 {
|
||||
addresses = []string{p.Endpoint}
|
||||
} else {
|
||||
// get route for endpoint from router
|
||||
addr, err := p.getRoute(ctx, p.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// set the address
|
||||
routes = addr
|
||||
// set the name
|
||||
service = p.Endpoint
|
||||
}
|
||||
} else {
|
||||
// no endpoint was specified just lookup the route
|
||||
// get route for endpoint from router
|
||||
addr, err := p.getRoute(ctx, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
routes = addr
|
||||
}
|
||||
|
||||
//nolint:prealloc
|
||||
opts := []client.CallOption{
|
||||
// set strategy to round robin
|
||||
client.WithSelector(p.Selector),
|
||||
}
|
||||
|
||||
// if the address is already set just serve it
|
||||
// TODO: figure it out if we should know to pick a link
|
||||
if len(addresses) > 0 {
|
||||
opts = append(opts,
|
||||
client.WithAddress(addresses...),
|
||||
)
|
||||
|
||||
// serve the normal way
|
||||
return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp, opts...)
|
||||
}
|
||||
|
||||
// there's no links e.g we're local routing then just serve it with addresses
|
||||
if local {
|
||||
var opts []client.CallOption
|
||||
|
||||
// set address if available via routes or specific endpoint
|
||||
if len(routes) > 0 {
|
||||
addresses = toNodes(routes)
|
||||
opts = append(opts, client.WithAddress(addresses...))
|
||||
}
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy calling %+v\n", addresses)
|
||||
}
|
||||
// serve the normal way
|
||||
return p.serveRequest(ctx, p.Client, service, endpoint, req, rsp, opts...)
|
||||
}
|
||||
|
||||
// we're assuming we need routes to operate on
|
||||
if len(routes) == 0 {
|
||||
return errors.InternalServerError("go.micro.proxy", "route not found")
|
||||
}
|
||||
|
||||
var gerr error
|
||||
|
||||
// we're routing globally with multiple links
|
||||
// so we need to pick a link per route
|
||||
for _, route := range routes {
|
||||
// pick the link or error out
|
||||
link, err := p.getLink(route)
|
||||
if err != nil {
|
||||
// ok let's try again
|
||||
gerr = err
|
||||
continue
|
||||
}
|
||||
|
||||
if logger.V(logger.TraceLevel, logger.DefaultLogger) {
|
||||
logger.Tracef("Proxy using route %+v\n", route)
|
||||
}
|
||||
|
||||
// set the address to call
|
||||
addresses := toNodes([]router.Route{route})
|
||||
// set the address in the options
|
||||
// disable retries since its one route processing
|
||||
opts = append(opts,
|
||||
client.WithAddress(addresses...),
|
||||
client.WithRetries(0),
|
||||
)
|
||||
|
||||
// do the request with the link
|
||||
gerr = p.serveRequest(ctx, link, service, endpoint, req, rsp, opts...)
|
||||
// return on no error since we succeeded
|
||||
if gerr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// return where the context deadline was exceeded
|
||||
if gerr == context.Canceled || gerr == context.DeadlineExceeded {
|
||||
return err
|
||||
}
|
||||
|
||||
// otherwise attempt to do it all over again
|
||||
}
|
||||
|
||||
// if we got here something went really badly wrong
|
||||
return gerr
|
||||
}
|
||||
|
||||
func (p *Proxy) serveRequest(ctx context.Context, link client.Client, service, endpoint string, req server.Request, rsp server.Response, opts ...client.CallOption) error {
|
||||
// read initial request
|
||||
body, err := req.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new request with raw bytes body
|
||||
creq := link.NewRequest(service, endpoint, &bytes.Frame{Data: body}, client.WithContentType(req.ContentType()))
|
||||
|
||||
// not a stream so make a client.Call request
|
||||
if !req.Stream() {
|
||||
crsp := new(bytes.Frame)
|
||||
|
||||
// make a call to the backend
|
||||
if err := link.Call(ctx, creq, crsp, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write the response
|
||||
if err := rsp.Write(crsp.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// new context with cancel
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// create new stream
|
||||
stream, err := link.Stream(ctx, creq, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
// if we receive a grpc stream we have to refire the initial request
|
||||
c, ok := req.Codec().(codec.Codec)
|
||||
if ok && c.String() == "grpc" && link.String() == "grpc" {
|
||||
// get the header from client
|
||||
hdr := req.Header()
|
||||
msg := &codec.Message{
|
||||
Type: codec.Request,
|
||||
Header: hdr,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
// write the raw request
|
||||
err = stream.Request().Codec().Write(msg, nil)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// create client request read loop if streaming
|
||||
go func() {
|
||||
err := readLoop(req, stream)
|
||||
if err != nil && err != io.EOF {
|
||||
// cancel the context
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
// get raw response
|
||||
resp := stream.Response()
|
||||
|
||||
// create server response write loop
|
||||
for {
|
||||
// read backend response body
|
||||
body, err := resp.Read()
|
||||
if err != nil {
|
||||
// when we're done if its a grpc stream we have to set the trailer
|
||||
if cc, ok := stream.(grpc.ClientStream); ok {
|
||||
if ss, ok := resp.Codec().(grpc.ServerStream); ok {
|
||||
ss.SetTrailer(cc.Trailer())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read backend response header
|
||||
hdr := resp.Header()
|
||||
|
||||
// write raw response header to client
|
||||
rsp.WriteHeader(hdr)
|
||||
|
||||
// write raw response body to client
|
||||
err = rsp.Write(body)
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) String() string {
|
||||
return "mucp"
|
||||
}
|
||||
|
||||
// NewSingleHostProxy returns a proxy which sends requests to a single backend
|
||||
func NewSingleHostProxy(endpoint string) *Proxy {
|
||||
return &Proxy{
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// NewProxy returns a new proxy which will route based on mucp headers
|
||||
func NewProxy(opts ...proxy.Option) proxy.Proxy {
|
||||
var options proxy.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
p := new(Proxy)
|
||||
p.Links = map[string]client.Client{}
|
||||
p.Routes = make(map[string]map[uint64]router.Route)
|
||||
p.options = options
|
||||
|
||||
// get endpoint
|
||||
p.Endpoint = options.Endpoint
|
||||
// set the client
|
||||
p.Client = options.Client
|
||||
// get router
|
||||
p.Router = options.Router
|
||||
|
||||
// set the default client
|
||||
if p.Client == nil {
|
||||
p.Client = grpcc.NewClient()
|
||||
}
|
||||
|
||||
// create default router and start it
|
||||
if p.Router == nil {
|
||||
p.Router = registry.NewRouter()
|
||||
}
|
||||
|
||||
if p.Selector == nil {
|
||||
p.Selector = roundrobin.NewSelector()
|
||||
}
|
||||
|
||||
// set the links
|
||||
if options.Links != nil {
|
||||
// get client
|
||||
p.Links = options.Links
|
||||
}
|
||||
|
||||
// TODO: remove this cruft
|
||||
// skip watching routes if proxy is set
|
||||
if len(p.Client.Options().Proxy) > 0 {
|
||||
return p
|
||||
}
|
||||
|
||||
go func() {
|
||||
// continuously attempt to watch routes
|
||||
for {
|
||||
// watch the routes
|
||||
p.watchRoutes()
|
||||
// in case of failure just wait a second
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
// TODO: speed up refreshing of metrics
|
||||
// without this ticking effort e.g stream
|
||||
t := time.NewTicker(time.Second * 10)
|
||||
defer t.Stop()
|
||||
|
||||
// we must refresh route metrics since they do not trigger new events
|
||||
for range t.C {
|
||||
// refresh route metrics
|
||||
p.refreshMetrics()
|
||||
}
|
||||
}()
|
||||
|
||||
return p
|
||||
}
|
||||
Reference in New Issue
Block a user