Initial QSfera import
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# GRPC Client
|
||||
|
||||
The grpc client is a [micro.Client](https://godoc.org/github.com/micro/go-micro/client#Client) compatible client.
|
||||
|
||||
## Overview
|
||||
|
||||
The client makes use of the [google.golang.org/grpc](google.golang.org/grpc) framework for the underlying communication mechanism.
|
||||
|
||||
## Usage
|
||||
|
||||
Specify the client to your micro service
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/micro/go-micro"
|
||||
"github.com/micro/go-plugins/client/grpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
micro.Client(grpc.NewClient()),
|
||||
)
|
||||
}
|
||||
```
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
b "bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/codec/bytes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/runtime/protoiface"
|
||||
"google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
type jsonCodec struct{}
|
||||
type protoCodec struct{}
|
||||
type bytesCodec struct{}
|
||||
type wrapCodec struct{ encoding.Codec }
|
||||
|
||||
var useNumber bool
|
||||
|
||||
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{},
|
||||
}
|
||||
)
|
||||
|
||||
// UseNumber fix unmarshal Number(8234567890123456789) to interface(8.234567890123457e+18).
|
||||
func UseNumber() {
|
||||
useNumber = true
|
||||
}
|
||||
|
||||
func (w wrapCodec) String() string {
|
||||
return w.Codec.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
|
||||
}
|
||||
return w.Codec.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (protoCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
switch m := v.(type) {
|
||||
case *bytes.Frame:
|
||||
return m.Data, nil
|
||||
case proto.Message:
|
||||
return proto.Marshal(m)
|
||||
case protoiface.MessageV1:
|
||||
// #2333 compatible with etcd legacy proto.Message
|
||||
m2 := protoimpl.X.ProtoMessageV2Of(m)
|
||||
return proto.Marshal(m2)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to marshal: %v is not type of *bytes.Frame or proto.Message", v)
|
||||
}
|
||||
|
||||
func (protoCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
switch m := v.(type) {
|
||||
case proto.Message:
|
||||
return proto.Unmarshal(data, m)
|
||||
case protoiface.MessageV1:
|
||||
// #2333 compatible with etcd legacy proto.Message
|
||||
m2 := protoimpl.X.ProtoMessageV2Of(m)
|
||||
return proto.Unmarshal(data, m2)
|
||||
}
|
||||
return fmt.Errorf("failed to unmarshal: %v is not type of proto.Message", v)
|
||||
}
|
||||
|
||||
func (protoCodec) Name() string {
|
||||
return "proto"
|
||||
}
|
||||
|
||||
func (bytesCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
b, ok := v.(*[]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to marshal: %v is not type of *[]byte", v)
|
||||
}
|
||||
return *b, nil
|
||||
}
|
||||
|
||||
func (bytesCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
b, ok := v.(*[]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to unmarshal: %v is not type of *[]byte", v)
|
||||
}
|
||||
*b = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bytesCodec) Name() string {
|
||||
return "bytes"
|
||||
}
|
||||
|
||||
func (jsonCodec) Marshal(v interface{}) ([]byte, error) {
|
||||
if b, ok := v.(*bytes.Frame); ok {
|
||||
return b.Data, nil
|
||||
}
|
||||
|
||||
if pb, ok := v.(proto.Message); ok {
|
||||
bytes, err := protojson.Marshal(pb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (jsonCodec) Unmarshal(data []byte, v interface{}) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if b, ok := v.(*bytes.Frame); ok {
|
||||
b.Data = data
|
||||
return nil
|
||||
}
|
||||
if pb, ok := v.(proto.Message); ok {
|
||||
return protojson.Unmarshal(data, pb)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(b.NewReader(data))
|
||||
if useNumber {
|
||||
dec.UseNumber()
|
||||
}
|
||||
return dec.Decode(v)
|
||||
}
|
||||
|
||||
func (jsonCodec) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
type grpcCodec struct {
|
||||
// headers
|
||||
id string
|
||||
target string
|
||||
method string
|
||||
endpoint string
|
||||
|
||||
s grpc.ClientStream
|
||||
c encoding.Codec
|
||||
}
|
||||
|
||||
func (g *grpcCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error {
|
||||
md, err := g.s.Header()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
return g.s.SendMsg(v)
|
||||
}
|
||||
// write the body using the framing codec
|
||||
return g.s.SendMsg(&bytes.Frame{Data: m.Body})
|
||||
}
|
||||
|
||||
func (g *grpcCodec) Close() error {
|
||||
return g.s.CloseSend()
|
||||
}
|
||||
|
||||
func (g *grpcCodec) String() string {
|
||||
return g.c.Name()
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v4/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func microError(err error) error {
|
||||
// no error
|
||||
switch err {
|
||||
case nil:
|
||||
return nil
|
||||
}
|
||||
|
||||
if verr, ok := err.(*errors.Error); ok {
|
||||
return verr
|
||||
}
|
||||
|
||||
// grpc error
|
||||
s, ok := status.FromError(err)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// return first error from details
|
||||
if details := s.Details(); len(details) > 0 {
|
||||
return microError(details[0].(error))
|
||||
}
|
||||
|
||||
// try to decode micro *errors.Error
|
||||
if e := errors.Parse(s.Message()); e.Code > 0 {
|
||||
return e // actually a micro error
|
||||
}
|
||||
|
||||
// fallback
|
||||
return errors.New("go.micro.client", s.Message(), microStatusFromGrpcCode(s.Code()))
|
||||
}
|
||||
|
||||
func microStatusFromGrpcCode(code codes.Code) int32 {
|
||||
switch code {
|
||||
case codes.OK:
|
||||
return http.StatusOK
|
||||
case codes.InvalidArgument:
|
||||
return http.StatusBadRequest
|
||||
case codes.DeadlineExceeded:
|
||||
return http.StatusRequestTimeout
|
||||
case codes.NotFound:
|
||||
return http.StatusNotFound
|
||||
case codes.AlreadyExists:
|
||||
return http.StatusConflict
|
||||
case codes.PermissionDenied:
|
||||
return http.StatusForbidden
|
||||
case codes.Unauthenticated:
|
||||
return http.StatusUnauthorized
|
||||
case codes.FailedPrecondition:
|
||||
return http.StatusPreconditionFailed
|
||||
case codes.Unimplemented:
|
||||
return http.StatusNotImplemented
|
||||
case codes.Internal:
|
||||
return http.StatusInternalServerError
|
||||
case codes.Unavailable:
|
||||
return http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
+709
@@ -0,0 +1,709 @@
|
||||
// Package grpc provides a gRPC client
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/client"
|
||||
raw "go-micro.dev/v4/codec/bytes"
|
||||
"go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/selector"
|
||||
"go-micro.dev/v4/util/cmd"
|
||||
pnet "go-micro.dev/v4/util/net"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/encoding"
|
||||
gmetadata "google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type grpcClient struct {
|
||||
opts client.Options
|
||||
pool *pool
|
||||
once atomic.Value
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultClients["grpc"] = NewClient
|
||||
|
||||
encoding.RegisterCodec(wrapCodec{jsonCodec{}})
|
||||
encoding.RegisterCodec(wrapCodec{protoCodec{}})
|
||||
encoding.RegisterCodec(wrapCodec{bytesCodec{}})
|
||||
}
|
||||
|
||||
// secure returns the dial option for whether its a secure or insecure connection.
|
||||
func (g *grpcClient) secure(addr string) grpc.DialOption {
|
||||
// first we check if theres'a tls config
|
||||
if g.opts.Context != nil {
|
||||
if v := g.opts.Context.Value(tlsAuth{}); v != nil {
|
||||
tls := v.(*tls.Config)
|
||||
creds := credentials.NewTLS(tls)
|
||||
// return tls config if it exists
|
||||
return grpc.WithTransportCredentials(creds)
|
||||
}
|
||||
}
|
||||
|
||||
// default config
|
||||
tlsConfig := &tls.Config{}
|
||||
defaultCreds := grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
|
||||
|
||||
// check if the address is prepended with https
|
||||
if strings.HasPrefix(addr, "https://") {
|
||||
return defaultCreds
|
||||
}
|
||||
|
||||
// if no port is specified or port is 443 default to tls
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
// assuming with no port its going to be secured
|
||||
if port == "443" {
|
||||
return defaultCreds
|
||||
} else if err != nil && strings.Contains(err.Error(), "missing port in address") {
|
||||
return defaultCreds
|
||||
}
|
||||
|
||||
// other fallback to insecure
|
||||
return grpc.WithInsecure()
|
||||
}
|
||||
|
||||
func (g *grpcClient) next(request client.Request, opts client.CallOptions) (selector.Next, error) {
|
||||
service, address, _ := pnet.Proxy(request.Service(), opts.Address)
|
||||
|
||||
// return remote address
|
||||
if len(address) > 0 {
|
||||
return func() (*registry.Node, error) {
|
||||
return ®istry.Node{
|
||||
Address: address[0],
|
||||
}, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// get next nodes from the selector
|
||||
next, err := g.opts.Selector.Select(service, opts.SelectOptions...)
|
||||
if err != nil {
|
||||
if err == selector.ErrNotFound {
|
||||
return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
|
||||
}
|
||||
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||
var header map[string]string
|
||||
|
||||
address := node.Address
|
||||
|
||||
if md, ok := metadata.FromContext(ctx); ok {
|
||||
header = make(map[string]string, len(md))
|
||||
for k, v := range md {
|
||||
header[strings.ToLower(k)] = v
|
||||
}
|
||||
} else {
|
||||
header = make(map[string]string)
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
header["timeout"] = fmt.Sprintf("%d", opts.RequestTimeout)
|
||||
// set the content type for the request
|
||||
header["x-content-type"] = req.ContentType()
|
||||
|
||||
md := gmetadata.New(header)
|
||||
ctx = gmetadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
cf, err := g.newGRPCCodec(req.ContentType())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
maxRecvMsgSize := g.maxRecvMsgSizeValue()
|
||||
maxSendMsgSize := g.maxSendMsgSizeValue()
|
||||
|
||||
var grr error
|
||||
|
||||
var dialCtx context.Context
|
||||
var cancel context.CancelFunc
|
||||
if opts.DialTimeout > 0 {
|
||||
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
|
||||
} else {
|
||||
dialCtx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
grpcDialOptions := []grpc.DialOption{
|
||||
g.secure(address),
|
||||
grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(maxRecvMsgSize),
|
||||
grpc.MaxCallSendMsgSize(maxSendMsgSize),
|
||||
),
|
||||
}
|
||||
|
||||
if opts := g.getGrpcDialOptions(); opts != nil {
|
||||
grpcDialOptions = append(grpcDialOptions, opts...)
|
||||
}
|
||||
|
||||
cc, err := g.pool.getConn(dialCtx, address, grpcDialOptions...)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
|
||||
}
|
||||
defer func() {
|
||||
// defer execution of release
|
||||
g.pool.release(address, cc, grr)
|
||||
}()
|
||||
|
||||
ch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
grpcCallOptions := []grpc.CallOption{
|
||||
grpc.ForceCodec(cf),
|
||||
grpc.CallContentSubtype(cf.Name())}
|
||||
if opts := callOpts(opts); opts != nil {
|
||||
grpcCallOptions = append(grpcCallOptions, opts...)
|
||||
}
|
||||
err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...)
|
||||
ch <- microError(err)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
grr = err
|
||||
case <-ctx.Done():
|
||||
grr = errors.Timeout("go.micro.client", "%v", ctx.Err())
|
||||
}
|
||||
|
||||
return grr
|
||||
}
|
||||
|
||||
func (g *grpcClient) stream(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||
var header map[string]string
|
||||
|
||||
address := node.Address
|
||||
|
||||
if md, ok := metadata.FromContext(ctx); ok {
|
||||
header = make(map[string]string, len(md))
|
||||
for k, v := range md {
|
||||
header[k] = v
|
||||
}
|
||||
} else {
|
||||
header = make(map[string]string)
|
||||
}
|
||||
|
||||
// set timeout in nanoseconds
|
||||
if opts.StreamTimeout > time.Duration(0) {
|
||||
header["timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
|
||||
}
|
||||
// set the content type for the request
|
||||
header["x-content-type"] = req.ContentType()
|
||||
|
||||
md := gmetadata.New(header)
|
||||
|
||||
// WebSocket connection adds the `Connection: Upgrade` header.
|
||||
// But as per the HTTP/2 spec, the `Connection` header makes the request malformed
|
||||
delete(md, "connection")
|
||||
|
||||
ctx = gmetadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
cf, err := g.newGRPCCodec(req.ContentType())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
var dialCtx context.Context
|
||||
var cancel context.CancelFunc
|
||||
if opts.DialTimeout > 0 {
|
||||
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
|
||||
} else {
|
||||
dialCtx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
wc := wrapCodec{cf}
|
||||
|
||||
grpcDialOptions := []grpc.DialOption{
|
||||
g.secure(address),
|
||||
}
|
||||
|
||||
if opts := g.getGrpcDialOptions(); opts != nil {
|
||||
grpcDialOptions = append(grpcDialOptions, opts...)
|
||||
}
|
||||
|
||||
cc, err := g.pool.getConn(dialCtx, address, grpcDialOptions...)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
|
||||
}
|
||||
|
||||
desc := &grpc.StreamDesc{
|
||||
StreamName: req.Service() + req.Endpoint(),
|
||||
ClientStreams: true,
|
||||
ServerStreams: true,
|
||||
}
|
||||
|
||||
grpcCallOptions := []grpc.CallOption{
|
||||
grpc.ForceCodec(wc),
|
||||
grpc.CallContentSubtype(cf.Name()),
|
||||
}
|
||||
if opts := callOpts(opts); opts != nil {
|
||||
grpcCallOptions = append(grpcCallOptions, opts...)
|
||||
}
|
||||
|
||||
// create a new canceling context
|
||||
newCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
st, err := cc.NewStream(newCtx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...)
|
||||
if err != nil {
|
||||
// we need to cleanup as we dialed and created a context
|
||||
// cancel the context
|
||||
cancel()
|
||||
// close the connection
|
||||
cc.Close()
|
||||
// now return the error
|
||||
return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err))
|
||||
}
|
||||
|
||||
codec := &grpcCodec{
|
||||
s: st,
|
||||
c: wc,
|
||||
}
|
||||
|
||||
// set request codec
|
||||
if r, ok := req.(*grpcRequest); ok {
|
||||
r.codec = codec
|
||||
}
|
||||
|
||||
// setup the stream response
|
||||
stream := &grpcStream{
|
||||
context: ctx,
|
||||
request: req,
|
||||
response: &response{
|
||||
conn: cc.ClientConn,
|
||||
stream: st,
|
||||
codec: cf,
|
||||
gcodec: codec,
|
||||
},
|
||||
stream: st,
|
||||
cancel: cancel,
|
||||
release: func(err error) {
|
||||
g.pool.release(address, cc, err)
|
||||
},
|
||||
}
|
||||
|
||||
// set the stream as the response
|
||||
val := reflect.ValueOf(rsp).Elem()
|
||||
val.Set(reflect.ValueOf(stream).Elem())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *grpcClient) poolMaxStreams() int {
|
||||
if g.opts.Context == nil {
|
||||
return DefaultPoolMaxStreams
|
||||
}
|
||||
v := g.opts.Context.Value(poolMaxStreams{})
|
||||
if v == nil {
|
||||
return DefaultPoolMaxStreams
|
||||
}
|
||||
return v.(int)
|
||||
}
|
||||
|
||||
func (g *grpcClient) poolMaxIdle() int {
|
||||
if g.opts.Context == nil {
|
||||
return DefaultPoolMaxIdle
|
||||
}
|
||||
v := g.opts.Context.Value(poolMaxIdle{})
|
||||
if v == nil {
|
||||
return DefaultPoolMaxIdle
|
||||
}
|
||||
return v.(int)
|
||||
}
|
||||
|
||||
func (g *grpcClient) maxRecvMsgSizeValue() int {
|
||||
if g.opts.Context == nil {
|
||||
return DefaultMaxRecvMsgSize
|
||||
}
|
||||
v := g.opts.Context.Value(maxRecvMsgSizeKey{})
|
||||
if v == nil {
|
||||
return DefaultMaxRecvMsgSize
|
||||
}
|
||||
return v.(int)
|
||||
}
|
||||
|
||||
func (g *grpcClient) maxSendMsgSizeValue() int {
|
||||
if g.opts.Context == nil {
|
||||
return DefaultMaxSendMsgSize
|
||||
}
|
||||
v := g.opts.Context.Value(maxSendMsgSizeKey{})
|
||||
if v == nil {
|
||||
return DefaultMaxSendMsgSize
|
||||
}
|
||||
return v.(int)
|
||||
}
|
||||
|
||||
func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) {
|
||||
codecs := make(map[string]encoding.Codec)
|
||||
if g.opts.Context != nil {
|
||||
if v := g.opts.Context.Value(codecsKey{}); v != nil {
|
||||
codecs = v.(map[string]encoding.Codec)
|
||||
}
|
||||
}
|
||||
if c, ok := codecs[contentType]; ok {
|
||||
return wrapCodec{c}, nil
|
||||
}
|
||||
if c, ok := defaultGRPCCodecs[contentType]; ok {
|
||||
return wrapCodec{c}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
func (g *grpcClient) Init(opts ...client.Option) error {
|
||||
size := g.opts.PoolSize
|
||||
ttl := g.opts.PoolTTL
|
||||
|
||||
for _, o := range opts {
|
||||
o(&g.opts)
|
||||
}
|
||||
|
||||
// update pool configuration if the options changed
|
||||
if size != g.opts.PoolSize || ttl != g.opts.PoolTTL {
|
||||
g.pool.Lock()
|
||||
g.pool.size = g.opts.PoolSize
|
||||
g.pool.ttl = int64(g.opts.PoolTTL.Seconds())
|
||||
g.pool.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *grpcClient) Options() client.Options {
|
||||
return g.opts
|
||||
}
|
||||
|
||||
func (g *grpcClient) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
|
||||
return newGRPCEvent(topic, msg, g.opts.ContentType, opts...)
|
||||
}
|
||||
|
||||
func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
|
||||
return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...)
|
||||
}
|
||||
|
||||
func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
if req == nil {
|
||||
return errors.InternalServerError("go.micro.client", "req is nil")
|
||||
} else if rsp == nil {
|
||||
return errors.InternalServerError("go.micro.client", "rsp is nil")
|
||||
}
|
||||
// make a copy of call opts
|
||||
callOpts := g.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
next, err := g.next(req, callOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if we already have a deadline
|
||||
d, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
// no deadline so we create a new one
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
|
||||
defer cancel()
|
||||
} else {
|
||||
// got a deadline so no need to setup context
|
||||
// but we need to set the timeout we pass along
|
||||
opt := client.WithRequestTimeout(time.Until(d))
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
// should we noop right here?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
default:
|
||||
}
|
||||
|
||||
// make copy of call method
|
||||
gcall := g.call
|
||||
|
||||
// wrap the call in reverse
|
||||
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||
gcall = callOpts.CallWrappers[i-1](gcall)
|
||||
}
|
||||
|
||||
// return errors.New("go.micro.client", "request timeout", 408)
|
||||
call := func(i int) error {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, req, i)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
// select next node
|
||||
node, err := next()
|
||||
service := req.Service()
|
||||
if err != nil {
|
||||
if err == selector.ErrNotFound {
|
||||
return errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
return errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
|
||||
}
|
||||
|
||||
// make the call
|
||||
err = gcall(ctx, node, req, rsp, callOpts)
|
||||
g.opts.Selector.Mark(service, node, err)
|
||||
if verr, ok := err.(*errors.Error); ok {
|
||||
return verr
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
ch := make(chan error, callOpts.Retries+1)
|
||||
var gerr error
|
||||
|
||||
for i := 0; i <= callOpts.Retries; i++ {
|
||||
go func(i int) {
|
||||
ch <- call(i)
|
||||
}(i)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
case err := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return err
|
||||
}
|
||||
|
||||
gerr = err
|
||||
}
|
||||
}
|
||||
|
||||
return gerr
|
||||
}
|
||||
|
||||
func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
||||
// make a copy of call opts
|
||||
callOpts := g.opts.CallOptions
|
||||
for _, opt := range opts {
|
||||
opt(&callOpts)
|
||||
}
|
||||
|
||||
next, err := g.next(req, callOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// #200 - streams shouldn't have a request timeout set on the context
|
||||
|
||||
// should we noop right here?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
default:
|
||||
}
|
||||
|
||||
// make a copy of stream
|
||||
gstream := g.stream
|
||||
|
||||
// wrap the call in reverse
|
||||
for i := len(callOpts.CallWrappers); i > 0; i-- {
|
||||
gstream = callOpts.CallWrappers[i-1](gstream)
|
||||
}
|
||||
|
||||
call := func(i int) (client.Stream, error) {
|
||||
// call backoff first. Someone may want an initial start delay
|
||||
t, err := callOpts.Backoff(ctx, req, i)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// only sleep if greater than 0
|
||||
if t.Seconds() > 0 {
|
||||
time.Sleep(t)
|
||||
}
|
||||
|
||||
node, err := next()
|
||||
service := req.Service()
|
||||
if err != nil {
|
||||
if err == selector.ErrNotFound {
|
||||
return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
|
||||
}
|
||||
return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
|
||||
}
|
||||
|
||||
// make the call
|
||||
stream := &grpcStream{}
|
||||
err = g.stream(ctx, node, req, stream, callOpts)
|
||||
|
||||
g.opts.Selector.Mark(service, node, err)
|
||||
return stream, err
|
||||
}
|
||||
|
||||
type response struct {
|
||||
stream client.Stream
|
||||
err error
|
||||
}
|
||||
|
||||
ch := make(chan response, callOpts.Retries+1)
|
||||
var grr error
|
||||
|
||||
for i := 0; i <= callOpts.Retries; i++ {
|
||||
go func(i int) {
|
||||
s, err := call(i)
|
||||
ch <- response{s, err}
|
||||
}(i)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
|
||||
case rsp := <-ch:
|
||||
// if the call succeeded lets bail early
|
||||
if rsp.err == nil {
|
||||
return rsp.stream, nil
|
||||
}
|
||||
|
||||
retry, rerr := callOpts.Retry(ctx, req, i, err)
|
||||
if rerr != nil {
|
||||
return nil, rerr
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return nil, rsp.err
|
||||
}
|
||||
|
||||
grr = rsp.err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, grr
|
||||
}
|
||||
|
||||
func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
|
||||
var options client.PublishOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = make(map[string]string)
|
||||
}
|
||||
md["Content-Type"] = p.ContentType()
|
||||
md["Micro-Topic"] = p.Topic()
|
||||
|
||||
cf, err := g.newGRPCCodec(p.ContentType())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
var body []byte
|
||||
|
||||
// passed in raw data
|
||||
if d, ok := p.Payload().(*raw.Frame); ok {
|
||||
body = d.Data
|
||||
} else {
|
||||
// set the body
|
||||
b, err := cf.Marshal(p.Payload())
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
body = b
|
||||
}
|
||||
|
||||
if !g.once.Load().(bool) {
|
||||
if err = g.opts.Broker.Connect(); err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
g.once.Store(true)
|
||||
}
|
||||
|
||||
topic := p.Topic()
|
||||
|
||||
// get the exchange
|
||||
if len(options.Exchange) > 0 {
|
||||
topic = options.Exchange
|
||||
}
|
||||
|
||||
return g.opts.Broker.Publish(topic, &broker.Message{
|
||||
Header: md,
|
||||
Body: body,
|
||||
}, broker.PublishContext(options.Context))
|
||||
}
|
||||
|
||||
func (g *grpcClient) String() string {
|
||||
return "grpc"
|
||||
}
|
||||
|
||||
func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption {
|
||||
if g.opts.CallOptions.Context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := g.opts.CallOptions.Context.Value(grpcDialOptions{})
|
||||
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
opts, ok := v.([]grpc.DialOption)
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func newClient(opts ...client.Option) client.Client {
|
||||
options := client.NewOptions()
|
||||
// default content type for grpc
|
||||
options.ContentType = "application/grpc+proto"
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
rc := &grpcClient{
|
||||
opts: options,
|
||||
}
|
||||
rc.once.Store(false)
|
||||
|
||||
rc.pool = newPool(options.PoolSize, options.PoolTTL, rc.poolMaxIdle(), rc.poolMaxStreams())
|
||||
|
||||
c := client.Client(rc)
|
||||
|
||||
// wrap in reverse
|
||||
for i := len(options.Wrappers); i > 0; i-- {
|
||||
c = options.Wrappers[i-1](c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func NewClient(opts ...client.Option) client.Client {
|
||||
return newClient(opts...)
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
type pool struct {
|
||||
size int
|
||||
ttl int64
|
||||
|
||||
// max streams on a *poolConn
|
||||
maxStreams int
|
||||
// max idle conns
|
||||
maxIdle int
|
||||
|
||||
sync.Mutex
|
||||
conns map[string]*streamsPool
|
||||
}
|
||||
|
||||
type streamsPool struct {
|
||||
// head of list
|
||||
head *poolConn
|
||||
// busy conns list
|
||||
busy *poolConn
|
||||
// the size of list
|
||||
count int
|
||||
// idle conn
|
||||
idle int
|
||||
}
|
||||
|
||||
type poolConn struct {
|
||||
// grpc conn
|
||||
*grpc.ClientConn
|
||||
err error
|
||||
addr string
|
||||
|
||||
// pool and streams pool
|
||||
pool *pool
|
||||
sp *streamsPool
|
||||
streams int
|
||||
created int64
|
||||
|
||||
// list
|
||||
pre *poolConn
|
||||
next *poolConn
|
||||
in bool
|
||||
}
|
||||
|
||||
func newPool(size int, ttl time.Duration, idle int, ms int) *pool {
|
||||
if ms <= 0 {
|
||||
ms = 1
|
||||
}
|
||||
if idle < 0 {
|
||||
idle = 0
|
||||
}
|
||||
return &pool{
|
||||
size: size,
|
||||
ttl: int64(ttl.Seconds()),
|
||||
maxStreams: ms,
|
||||
maxIdle: idle,
|
||||
conns: make(map[string]*streamsPool),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pool) getConn(dialCtx context.Context, addr string, opts ...grpc.DialOption) (*poolConn, error) {
|
||||
now := time.Now().Unix()
|
||||
p.Lock()
|
||||
sp, ok := p.conns[addr]
|
||||
if !ok {
|
||||
sp = &streamsPool{head: &poolConn{}, busy: &poolConn{}, count: 0, idle: 0}
|
||||
p.conns[addr] = sp
|
||||
}
|
||||
// while we have conns check streams and then return one
|
||||
// otherwise we'll create a new conn
|
||||
conn := sp.head.next
|
||||
for conn != nil {
|
||||
// check conn state
|
||||
// https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md
|
||||
switch conn.GetState() {
|
||||
case connectivity.Connecting:
|
||||
conn = conn.next
|
||||
continue
|
||||
case connectivity.Shutdown:
|
||||
next := conn.next
|
||||
if conn.streams == 0 {
|
||||
removeConn(conn)
|
||||
sp.idle--
|
||||
}
|
||||
conn = next
|
||||
continue
|
||||
case connectivity.TransientFailure:
|
||||
next := conn.next
|
||||
if conn.streams == 0 {
|
||||
removeConn(conn)
|
||||
conn.ClientConn.Close()
|
||||
sp.idle--
|
||||
}
|
||||
conn = next
|
||||
continue
|
||||
case connectivity.Ready:
|
||||
case connectivity.Idle:
|
||||
}
|
||||
// a old conn
|
||||
if now-conn.created > p.ttl {
|
||||
next := conn.next
|
||||
if conn.streams == 0 {
|
||||
removeConn(conn)
|
||||
conn.ClientConn.Close()
|
||||
sp.idle--
|
||||
}
|
||||
conn = next
|
||||
continue
|
||||
}
|
||||
// a busy conn
|
||||
if conn.streams >= p.maxStreams {
|
||||
next := conn.next
|
||||
removeConn(conn)
|
||||
addConnAfter(conn, sp.busy)
|
||||
conn = next
|
||||
continue
|
||||
}
|
||||
// a idle conn
|
||||
if conn.streams == 0 {
|
||||
sp.idle--
|
||||
}
|
||||
// a good conn
|
||||
conn.streams++
|
||||
p.Unlock()
|
||||
return conn, nil
|
||||
}
|
||||
p.Unlock()
|
||||
|
||||
// create new conn
|
||||
cc, err := grpc.DialContext(dialCtx, addr, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn = &poolConn{cc, nil, addr, p, sp, 1, time.Now().Unix(), nil, nil, false}
|
||||
|
||||
// add conn to streams pool
|
||||
p.Lock()
|
||||
if sp.count < p.size {
|
||||
addConnAfter(conn, sp.head)
|
||||
}
|
||||
p.Unlock()
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (p *pool) release(addr string, conn *poolConn, err error) {
|
||||
p.Lock()
|
||||
p, sp, created := conn.pool, conn.sp, conn.created
|
||||
// try to add conn
|
||||
if !conn.in && sp.count < p.size {
|
||||
addConnAfter(conn, sp.head)
|
||||
}
|
||||
if !conn.in {
|
||||
p.Unlock()
|
||||
conn.ClientConn.Close()
|
||||
return
|
||||
}
|
||||
// a busy conn
|
||||
if conn.streams >= p.maxStreams {
|
||||
removeConn(conn)
|
||||
addConnAfter(conn, sp.head)
|
||||
}
|
||||
conn.streams--
|
||||
// if streams == 0, we can do something
|
||||
if conn.streams == 0 {
|
||||
// 1. it has errored
|
||||
// 2. too many idle conn or
|
||||
// 3. conn is too old
|
||||
now := time.Now().Unix()
|
||||
if err != nil || sp.idle >= p.maxIdle || now-created > p.ttl {
|
||||
removeConn(conn)
|
||||
p.Unlock()
|
||||
conn.ClientConn.Close()
|
||||
return
|
||||
}
|
||||
sp.idle++
|
||||
}
|
||||
p.Unlock()
|
||||
}
|
||||
|
||||
func (conn *poolConn) Close() {
|
||||
conn.pool.release(conn.addr, conn, conn.err)
|
||||
}
|
||||
|
||||
func removeConn(conn *poolConn) {
|
||||
if conn.pre != nil {
|
||||
conn.pre.next = conn.next
|
||||
}
|
||||
if conn.next != nil {
|
||||
conn.next.pre = conn.pre
|
||||
}
|
||||
conn.pre = nil
|
||||
conn.next = nil
|
||||
conn.in = false
|
||||
conn.sp.count--
|
||||
return
|
||||
}
|
||||
|
||||
func addConnAfter(conn *poolConn, after *poolConn) {
|
||||
conn.next = after.next
|
||||
conn.pre = after
|
||||
if after.next != nil {
|
||||
after.next.pre = conn
|
||||
}
|
||||
after.next = conn
|
||||
conn.in = true
|
||||
conn.sp.count++
|
||||
return
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/client"
|
||||
)
|
||||
|
||||
type grpcEvent struct {
|
||||
topic string
|
||||
contentType string
|
||||
payload interface{}
|
||||
}
|
||||
|
||||
func newGRPCEvent(topic string, payload interface{}, contentType string, opts ...client.MessageOption) client.Message {
|
||||
var options client.MessageOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if len(options.ContentType) > 0 {
|
||||
contentType = options.ContentType
|
||||
}
|
||||
|
||||
return &grpcEvent{
|
||||
payload: payload,
|
||||
topic: topic,
|
||||
contentType: contentType,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *grpcEvent) ContentType() string {
|
||||
return g.contentType
|
||||
}
|
||||
|
||||
func (g *grpcEvent) Topic() string {
|
||||
return g.topic
|
||||
}
|
||||
|
||||
func (g *grpcEvent) Payload() interface{} {
|
||||
return g.payload
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Package grpc provides a gRPC options
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultPoolMaxStreams maximum streams on a connectioin
|
||||
// (20).
|
||||
DefaultPoolMaxStreams = 20
|
||||
|
||||
// DefaultPoolMaxIdle maximum idle conns of a pool
|
||||
// (50).
|
||||
DefaultPoolMaxIdle = 50
|
||||
|
||||
// DefaultMaxRecvMsgSize maximum message that client can receive
|
||||
// (4 MB).
|
||||
DefaultMaxRecvMsgSize = 1024 * 1024 * 4
|
||||
|
||||
// DefaultMaxSendMsgSize maximum message that client can send
|
||||
// (4 MB).
|
||||
DefaultMaxSendMsgSize = 1024 * 1024 * 4
|
||||
)
|
||||
|
||||
type poolMaxStreams struct{}
|
||||
type poolMaxIdle struct{}
|
||||
type codecsKey struct{}
|
||||
type tlsAuth struct{}
|
||||
type maxRecvMsgSizeKey struct{}
|
||||
type maxSendMsgSizeKey struct{}
|
||||
type grpcDialOptions struct{}
|
||||
type grpcCallOptions struct{}
|
||||
|
||||
// maximum streams on a connectioin.
|
||||
func PoolMaxStreams(n int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, poolMaxStreams{}, n)
|
||||
}
|
||||
}
|
||||
|
||||
// maximum idle conns of a pool.
|
||||
func PoolMaxIdle(d int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, poolMaxIdle{}, d)
|
||||
}
|
||||
}
|
||||
|
||||
// gRPC Codec to be used to encode/decode requests for a given content type.
|
||||
func Codec(contentType string, c encoding.Codec) client.Option {
|
||||
return func(o *client.Options) {
|
||||
codecs := make(map[string]encoding.Codec)
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
if v := o.Context.Value(codecsKey{}); v != nil {
|
||||
codecs = v.(map[string]encoding.Codec)
|
||||
}
|
||||
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) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, tlsAuth{}, t)
|
||||
}
|
||||
}
|
||||
|
||||
// MaxRecvMsgSize set the maximum size of message that client can receive.
|
||||
func MaxRecvMsgSize(s int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, maxRecvMsgSizeKey{}, s)
|
||||
}
|
||||
}
|
||||
|
||||
// MaxSendMsgSize set the maximum size of message that client can send.
|
||||
func MaxSendMsgSize(s int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, maxSendMsgSizeKey{}, s)
|
||||
}
|
||||
}
|
||||
|
||||
// DialOptions to be used to configure gRPC dial options.
|
||||
func DialOptions(opts ...grpc.DialOption) client.CallOption {
|
||||
return func(o *client.CallOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, grpcDialOptions{}, opts)
|
||||
}
|
||||
}
|
||||
|
||||
// CallOptions to be used to configure gRPC call options.
|
||||
func CallOptions(opts ...grpc.CallOption) client.CallOption {
|
||||
return func(o *client.CallOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, grpcCallOptions{}, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func callOpts(opts client.CallOptions) []grpc.CallOption {
|
||||
if opts.Context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := opts.Context.Value(grpcCallOptions{})
|
||||
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
options, ok := v.([]grpc.CallOption)
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/codec"
|
||||
)
|
||||
|
||||
type grpcRequest struct {
|
||||
service string
|
||||
method string
|
||||
contentType string
|
||||
request interface{}
|
||||
opts client.RequestOptions
|
||||
codec codec.Codec
|
||||
}
|
||||
|
||||
// service Struct.Method /service.Struct/Method.
|
||||
func methodToGRPC(service, method string) string {
|
||||
// no method or already grpc method
|
||||
if len(method) == 0 || method[0] == '/' {
|
||||
return method
|
||||
}
|
||||
|
||||
// assume method is Foo.Bar
|
||||
mParts := strings.Split(method, ".")
|
||||
if len(mParts) != 2 {
|
||||
return method
|
||||
}
|
||||
|
||||
if len(service) == 0 {
|
||||
return fmt.Sprintf("/%s/%s", mParts[0], mParts[1])
|
||||
}
|
||||
|
||||
// return /pkg.Foo/Bar
|
||||
return fmt.Sprintf("/%s.%s/%s", service, mParts[0], mParts[1])
|
||||
}
|
||||
|
||||
func newGRPCRequest(service, method string, request interface{}, contentType string, reqOpts ...client.RequestOption) client.Request {
|
||||
var opts client.RequestOptions
|
||||
for _, o := range reqOpts {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
// set the content-type specified
|
||||
if len(opts.ContentType) > 0 {
|
||||
contentType = opts.ContentType
|
||||
}
|
||||
|
||||
return &grpcRequest{
|
||||
service: service,
|
||||
method: method,
|
||||
request: request,
|
||||
contentType: contentType,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *grpcRequest) ContentType() string {
|
||||
return g.contentType
|
||||
}
|
||||
|
||||
func (g *grpcRequest) Service() string {
|
||||
return g.service
|
||||
}
|
||||
|
||||
func (g *grpcRequest) Method() string {
|
||||
return g.method
|
||||
}
|
||||
|
||||
func (g *grpcRequest) Endpoint() string {
|
||||
return g.method
|
||||
}
|
||||
|
||||
func (g *grpcRequest) Codec() codec.Writer {
|
||||
return g.codec
|
||||
}
|
||||
|
||||
func (g *grpcRequest) Body() interface{} {
|
||||
return g.request
|
||||
}
|
||||
|
||||
func (g *grpcRequest) Stream() bool {
|
||||
return g.opts.Stream
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/codec/bytes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
conn *grpc.ClientConn
|
||||
stream grpc.ClientStream
|
||||
codec encoding.Codec
|
||||
gcodec codec.Codec
|
||||
}
|
||||
|
||||
// Read the response.
|
||||
func (r *response) Codec() codec.Reader {
|
||||
return r.gcodec
|
||||
}
|
||||
|
||||
// read the header.
|
||||
func (r *response) Header() map[string]string {
|
||||
md, err := r.stream.Header()
|
||||
if err != nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
hdr := make(map[string]string, len(md))
|
||||
for k, v := range md {
|
||||
hdr[k] = strings.Join(v, ",")
|
||||
}
|
||||
return hdr
|
||||
}
|
||||
|
||||
// Read the undecoded response.
|
||||
func (r *response) Read() ([]byte, error) {
|
||||
f := &bytes.Frame{}
|
||||
if err := r.gcodec.ReadBody(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Data, nil
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Implements the streamer interface.
|
||||
type grpcStream struct {
|
||||
sync.RWMutex
|
||||
closed bool
|
||||
err error
|
||||
stream grpc.ClientStream
|
||||
request client.Request
|
||||
response client.Response
|
||||
context context.Context
|
||||
cancel func()
|
||||
release func(error)
|
||||
}
|
||||
|
||||
func (g *grpcStream) Context() context.Context {
|
||||
return g.context
|
||||
}
|
||||
|
||||
func (g *grpcStream) Request() client.Request {
|
||||
return g.request
|
||||
}
|
||||
|
||||
func (g *grpcStream) Response() client.Response {
|
||||
return g.response
|
||||
}
|
||||
|
||||
func (g *grpcStream) Send(msg interface{}) error {
|
||||
if err := g.stream.SendMsg(msg); err != nil {
|
||||
g.setError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *grpcStream) Recv(msg interface{}) (err error) {
|
||||
if err = g.stream.RecvMsg(msg); err != nil {
|
||||
if err != io.EOF {
|
||||
g.setError(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *grpcStream) Error() error {
|
||||
g.RLock()
|
||||
defer g.RUnlock()
|
||||
return g.err
|
||||
}
|
||||
|
||||
func (g *grpcStream) setError(e error) {
|
||||
g.Lock()
|
||||
g.err = e
|
||||
g.Unlock()
|
||||
}
|
||||
|
||||
func (g *grpcStream) CloseSend() error {
|
||||
return g.stream.CloseSend()
|
||||
}
|
||||
|
||||
func (g *grpcStream) Close() error {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
|
||||
if g.closed {
|
||||
return nil
|
||||
}
|
||||
// cancel the context
|
||||
g.cancel()
|
||||
g.closed = true
|
||||
// release back to pool
|
||||
g.release(g.err)
|
||||
return nil
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# NATS JetStream
|
||||
|
||||
This plugin uses NATS with JetStream to send and receive events.
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
// Package natsjs provides a NATS Jetstream implementation of the events.Stream interface.
|
||||
package natsjs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
nats "github.com/nats-io/nats.go"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go-micro.dev/v4/events"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClusterID = "micro"
|
||||
)
|
||||
|
||||
// NewStream returns an initialized nats stream or an error if the connection to the nats
|
||||
// server could not be established.
|
||||
func NewStream(opts ...Option) (events.Stream, error) {
|
||||
// parse the options
|
||||
options := Options{
|
||||
ClientID: uuid.New().String(),
|
||||
ClusterID: defaultClusterID,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
s := &stream{opts: options}
|
||||
|
||||
natsJetStreamCtx, err := connectToNatsJetStream(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error connecting to nats cluster %v: %w", options.ClusterID, err)
|
||||
}
|
||||
|
||||
s.natsJetStreamCtx = natsJetStreamCtx
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
opts Options
|
||||
natsJetStreamCtx nats.JetStreamContext
|
||||
}
|
||||
|
||||
func connectToNatsJetStream(options Options) (nats.JetStreamContext, error) {
|
||||
nopts := nats.GetDefaultOptions()
|
||||
if options.TLSConfig != nil {
|
||||
nopts.Secure = true
|
||||
nopts.TLSConfig = options.TLSConfig
|
||||
}
|
||||
|
||||
if options.NkeyConfig != "" {
|
||||
nopts.Nkey = options.NkeyConfig
|
||||
}
|
||||
|
||||
if len(options.Address) > 0 {
|
||||
nopts.Servers = strings.Split(options.Address, ",")
|
||||
}
|
||||
|
||||
if options.Name != "" {
|
||||
nopts.Name = options.Name
|
||||
}
|
||||
|
||||
if options.Username != "" && options.Password != "" {
|
||||
nopts.User = options.Username
|
||||
nopts.Password = options.Password
|
||||
}
|
||||
|
||||
conn, err := nopts.Connect()
|
||||
if err != nil {
|
||||
tls := nopts.TLSConfig != nil
|
||||
return nil, fmt.Errorf("error connecting to nats at %v with tls enabled (%v): %w", options.Address, tls, err)
|
||||
}
|
||||
|
||||
js, err := conn.JetStream()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while obtaining JetStream context: %w", err)
|
||||
}
|
||||
|
||||
return js, nil
|
||||
}
|
||||
|
||||
// Publish a message to a topic.
|
||||
func (s *stream) Publish(topic string, msg interface{}, opts ...events.PublishOption) error {
|
||||
// validate the topic
|
||||
if len(topic) == 0 {
|
||||
return events.ErrMissingTopic
|
||||
}
|
||||
|
||||
// parse the options
|
||||
options := events.PublishOptions{
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// encode the message if it's not already encoded
|
||||
var payload []byte
|
||||
if p, ok := msg.([]byte); ok {
|
||||
payload = p
|
||||
} else {
|
||||
p, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return events.ErrEncodingMessage
|
||||
}
|
||||
payload = p
|
||||
}
|
||||
|
||||
// construct the event
|
||||
event := &events.Event{
|
||||
ID: uuid.New().String(),
|
||||
Topic: topic,
|
||||
Timestamp: options.Timestamp,
|
||||
Metadata: options.Metadata,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
// serialize the event to bytes
|
||||
bytes, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error encoding event")
|
||||
}
|
||||
|
||||
// publish the event to the topic's channel
|
||||
// publish synchronously if configured
|
||||
if s.opts.SyncPublish {
|
||||
_, err := s.natsJetStreamCtx.Publish(event.Topic, bytes)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "Error publishing message to topic")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// publish asynchronously by default
|
||||
if _, err := s.natsJetStreamCtx.PublishAsync(event.Topic, bytes); err != nil {
|
||||
return errors.Wrap(err, "Error publishing message to topic")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Consume from a topic.
|
||||
func (s *stream) Consume(topic string, opts ...events.ConsumeOption) (<-chan events.Event, error) {
|
||||
// validate the topic
|
||||
if len(topic) == 0 {
|
||||
return nil, events.ErrMissingTopic
|
||||
}
|
||||
|
||||
log := s.opts.Logger
|
||||
|
||||
// parse the options
|
||||
options := events.ConsumeOptions{
|
||||
Group: uuid.New().String(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// setup the subscriber
|
||||
channel := make(chan events.Event)
|
||||
handleMsg := func(msg *nats.Msg) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
|
||||
// decode the message
|
||||
var evt events.Event
|
||||
if err := json.Unmarshal(msg.Data, &evt); err != nil {
|
||||
log.Logf(logger.ErrorLevel, "Error decoding message: %v", err)
|
||||
// not acknowledging the message is the way to indicate an error occurred
|
||||
return
|
||||
}
|
||||
|
||||
if !options.AutoAck {
|
||||
// set up the ack funcs
|
||||
evt.SetAckFunc(func() error {
|
||||
return msg.Ack()
|
||||
})
|
||||
evt.SetNackFunc(func() error {
|
||||
return msg.Nak()
|
||||
})
|
||||
}
|
||||
|
||||
// push onto the channel and wait for the consumer to take the event off before we acknowledge it.
|
||||
channel <- evt
|
||||
|
||||
if !options.AutoAck {
|
||||
return
|
||||
}
|
||||
|
||||
if err := msg.Ack(nats.Context(ctx)); err != nil {
|
||||
log.Logf(logger.ErrorLevel, "Error acknowledging message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that a stream exists for that topic
|
||||
_, err := s.natsJetStreamCtx.StreamInfo(topic)
|
||||
if err != nil {
|
||||
cfg := &nats.StreamConfig{
|
||||
Name: topic,
|
||||
}
|
||||
|
||||
_, err = s.natsJetStreamCtx.AddStream(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Stream did not exist and adding a stream failed")
|
||||
}
|
||||
}
|
||||
|
||||
// setup the options
|
||||
subOpts := []nats.SubOpt{}
|
||||
|
||||
if options.CustomRetries {
|
||||
subOpts = append(subOpts, nats.MaxDeliver(options.GetRetryLimit()))
|
||||
}
|
||||
|
||||
if options.AutoAck {
|
||||
subOpts = append(subOpts, nats.AckNone())
|
||||
} else {
|
||||
subOpts = append(subOpts, nats.AckExplicit())
|
||||
}
|
||||
|
||||
if !options.Offset.IsZero() {
|
||||
subOpts = append(subOpts, nats.StartTime(options.Offset))
|
||||
} else {
|
||||
subOpts = append(subOpts, nats.DeliverNew())
|
||||
}
|
||||
|
||||
if options.AckWait > 0 {
|
||||
subOpts = append(subOpts, nats.AckWait(options.AckWait))
|
||||
}
|
||||
|
||||
// connect the subscriber via a queue group only if durable streams are enabled
|
||||
if !s.opts.DisableDurableStreams {
|
||||
subOpts = append(subOpts, nats.Durable(options.Group))
|
||||
_, err = s.natsJetStreamCtx.QueueSubscribe(topic, options.Group, handleMsg, subOpts...)
|
||||
} else {
|
||||
subOpts = append(subOpts, nats.ConsumerName(options.Group))
|
||||
_, err = s.natsJetStreamCtx.Subscribe(topic, handleMsg, subOpts...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error subscribing to topic")
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package natsjs
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
// Options which are used to configure the nats stream.
|
||||
type Options struct {
|
||||
ClusterID string
|
||||
ClientID string
|
||||
Address string
|
||||
NkeyConfig string
|
||||
TLSConfig *tls.Config
|
||||
Logger logger.Logger
|
||||
SyncPublish bool
|
||||
Name string
|
||||
DisableDurableStreams bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Option is a function which configures options.
|
||||
type Option func(o *Options)
|
||||
|
||||
// ClusterID sets the cluster id for the nats connection.
|
||||
func ClusterID(id string) Option {
|
||||
return func(o *Options) {
|
||||
o.ClusterID = id
|
||||
}
|
||||
}
|
||||
|
||||
// ClientID sets the client id for the nats connection.
|
||||
func ClientID(id string) Option {
|
||||
return func(o *Options) {
|
||||
o.ClientID = id
|
||||
}
|
||||
}
|
||||
|
||||
// Address of the nats cluster.
|
||||
func Address(addr string) Option {
|
||||
return func(o *Options) {
|
||||
o.Address = addr
|
||||
}
|
||||
}
|
||||
|
||||
// TLSConfig to use when connecting to the cluster.
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSConfig = t
|
||||
}
|
||||
}
|
||||
|
||||
// NkeyConfig string to use when connecting to the cluster.
|
||||
func NkeyConfig(nkey string) Option {
|
||||
return func(o *Options) {
|
||||
o.NkeyConfig = nkey
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the underlying logger.
|
||||
func Logger(log logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = log
|
||||
}
|
||||
}
|
||||
|
||||
// SynchronousPublish allows using a synchronous publishing instead of the default asynchronous.
|
||||
func SynchronousPublish(sync bool) Option {
|
||||
return func(o *Options) {
|
||||
o.SyncPublish = sync
|
||||
}
|
||||
}
|
||||
|
||||
// Name allows to add a name to the natsjs connection.
|
||||
func Name(name string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// DisableDurableStreams will disable durable streams.
|
||||
func DisableDurableStreams() Option {
|
||||
return func(o *Options) {
|
||||
o.DisableDurableStreams = true
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate authenticates the connection with the given username and password.
|
||||
func Authenticate(username, password string) Option {
|
||||
return func(o *Options) {
|
||||
o.Username = username
|
||||
o.Password = password
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Zerolog
|
||||
|
||||
[Zerolog](https://github.com/rs/zerolog) logger implementation for __go-micro__ [meta logger](https://github.com/micro/go-micro/tree/master/logger).
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
func ExampleWithOut() {
|
||||
logger.DefaultLogger = zerolog.NewLogger(logger.WithOutput(os.Stdout), logger.WithLevel(logger.DebugLevel))
|
||||
|
||||
logger.Infof(logger.InfoLevel, "testing: %s", "Infof")
|
||||
|
||||
// Output:
|
||||
// {"level":"info","message":"testing: Infof"}
|
||||
}
|
||||
```
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
logger.Options
|
||||
|
||||
// Flag for whether to log caller info (off by default)
|
||||
ReportCaller bool
|
||||
// Use this logger as system wide default logger (off by default)
|
||||
UseAsDefault bool
|
||||
// zerolog hooks
|
||||
Hooks []zerolog.Hook
|
||||
// TimeFormat is one of time.RFC3339, time.RFC3339Nano, time.*
|
||||
TimeFormat string
|
||||
// Runtime mode. (Production by default)
|
||||
Mode Mode
|
||||
// Exit Function to call when FatalLevel log
|
||||
ExitFunc func(int)
|
||||
}
|
||||
|
||||
type reportCallerKey struct{}
|
||||
|
||||
func ReportCaller() logger.Option {
|
||||
return logger.SetOption(reportCallerKey{}, true)
|
||||
}
|
||||
|
||||
type useAsDefaultKey struct{}
|
||||
|
||||
func UseAsDefault() logger.Option {
|
||||
return logger.SetOption(useAsDefaultKey{}, true)
|
||||
}
|
||||
|
||||
type developmentModeKey struct{}
|
||||
|
||||
func WithDevelopmentMode() logger.Option {
|
||||
return logger.SetOption(developmentModeKey{}, true)
|
||||
}
|
||||
|
||||
type productionModeKey struct{}
|
||||
|
||||
func WithProductionMode() logger.Option {
|
||||
return logger.SetOption(productionModeKey{}, true)
|
||||
}
|
||||
|
||||
type timeFormatKey struct{}
|
||||
|
||||
func WithTimeFormat(timeFormat string) logger.Option {
|
||||
return logger.SetOption(timeFormatKey{}, timeFormat)
|
||||
}
|
||||
|
||||
type hooksKey struct{}
|
||||
|
||||
func WithHooks(hooks []zerolog.Hook) logger.Option {
|
||||
return logger.SetOption(hooksKey{}, hooks)
|
||||
}
|
||||
|
||||
type exitKey struct{}
|
||||
|
||||
func WithExitFunc(exit func(int)) logger.Option {
|
||||
return logger.SetOption(exitKey{}, exit)
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
zlog "github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
type Mode uint8
|
||||
|
||||
const (
|
||||
Production Mode = iota
|
||||
Development
|
||||
)
|
||||
|
||||
type zeroLogger struct {
|
||||
zLog zerolog.Logger
|
||||
opts Options
|
||||
}
|
||||
|
||||
func (l *zeroLogger) Init(opts ...logger.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&l.opts.Options)
|
||||
}
|
||||
|
||||
if hs, ok := l.opts.Context.Value(hooksKey{}).([]zerolog.Hook); ok {
|
||||
l.opts.Hooks = hs
|
||||
}
|
||||
if tf, ok := l.opts.Context.Value(timeFormatKey{}).(string); ok {
|
||||
l.opts.TimeFormat = tf
|
||||
}
|
||||
if exitFunction, ok := l.opts.Context.Value(exitKey{}).(func(int)); ok {
|
||||
l.opts.ExitFunc = exitFunction
|
||||
}
|
||||
if caller, ok := l.opts.Context.Value(reportCallerKey{}).(bool); ok && caller {
|
||||
l.opts.ReportCaller = caller
|
||||
}
|
||||
if useDefault, ok := l.opts.Context.Value(useAsDefaultKey{}).(bool); ok && useDefault {
|
||||
l.opts.UseAsDefault = useDefault
|
||||
}
|
||||
if devMode, ok := l.opts.Context.Value(developmentModeKey{}).(bool); ok && devMode {
|
||||
l.opts.Mode = Development
|
||||
}
|
||||
if prodMode, ok := l.opts.Context.Value(productionModeKey{}).(bool); ok && prodMode {
|
||||
l.opts.Mode = Production
|
||||
}
|
||||
|
||||
skip := 4
|
||||
if l.opts.CallerSkipCount > 0 {
|
||||
skip = l.opts.CallerSkipCount
|
||||
}
|
||||
// RESET
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
zerolog.ErrorStackMarshaler = nil
|
||||
zerolog.CallerSkipFrameCount = skip
|
||||
|
||||
switch l.opts.Mode {
|
||||
case Development:
|
||||
zerolog.ErrorStackMarshaler = func(err error) interface{} {
|
||||
fmt.Println(string(debug.Stack()))
|
||||
return nil
|
||||
}
|
||||
consOut := zerolog.NewConsoleWriter(
|
||||
func(w *zerolog.ConsoleWriter) {
|
||||
if len(l.opts.TimeFormat) > 0 {
|
||||
w.TimeFormat = l.opts.TimeFormat
|
||||
}
|
||||
w.Out = l.opts.Out
|
||||
w.NoColor = false
|
||||
},
|
||||
)
|
||||
// level = logger.DebugLevel
|
||||
l.zLog = zerolog.New(consOut).
|
||||
Level(zerolog.DebugLevel).
|
||||
With().Timestamp().Stack().Logger()
|
||||
default: // Production
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
l.zLog = zerolog.New(l.opts.Out).
|
||||
Level(zerolog.InfoLevel).
|
||||
With().Timestamp().Stack().Logger()
|
||||
}
|
||||
|
||||
// Set log Level if not default
|
||||
if l.opts.Level != 100 {
|
||||
zerolog.SetGlobalLevel(loggerToZerologLevel(l.opts.Level))
|
||||
l.zLog = l.zLog.Level(loggerToZerologLevel(l.opts.Level))
|
||||
}
|
||||
|
||||
// Reporting caller
|
||||
if l.opts.ReportCaller {
|
||||
l.zLog = l.zLog.With().Caller().Logger()
|
||||
}
|
||||
|
||||
// Adding hooks if exist
|
||||
for _, hook := range l.opts.Hooks {
|
||||
l.zLog = l.zLog.Hook(hook)
|
||||
}
|
||||
|
||||
// Setting timeFormat
|
||||
if len(l.opts.TimeFormat) > 0 {
|
||||
zerolog.TimeFieldFormat = l.opts.TimeFormat
|
||||
}
|
||||
|
||||
// Adding seed fields if exist
|
||||
if l.opts.Fields != nil {
|
||||
l.zLog = l.zLog.With().Fields(l.opts.Fields).Logger()
|
||||
}
|
||||
|
||||
// Also set it as zerolog's Default logger
|
||||
if l.opts.UseAsDefault {
|
||||
zlog.Logger = l.zLog
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *zeroLogger) Fields(fields map[string]interface{}) logger.Logger {
|
||||
l.zLog = l.zLog.With().Fields(fields).Logger()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *zeroLogger) Error(err error) logger.Logger {
|
||||
l.zLog = l.zLog.With().Fields(map[string]interface{}{zerolog.ErrorFieldName: err}).Logger()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *zeroLogger) Log(level logger.Level, args ...interface{}) {
|
||||
msg := fmt.Sprint(args...)
|
||||
l.zLog.WithLevel(loggerToZerologLevel(level)).Msg(msg)
|
||||
// Invoke os.Exit because unlike zerolog.Logger.Fatal zerolog.Logger.WithLevel won't stop the execution.
|
||||
if level == logger.FatalLevel {
|
||||
l.opts.ExitFunc(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *zeroLogger) Logf(level logger.Level, format string, args ...interface{}) {
|
||||
l.zLog.WithLevel(loggerToZerologLevel(level)).Msgf(format, args...)
|
||||
// Invoke os.Exit because unlike zerolog.Logger.Fatal zerolog.Logger.WithLevel won't stop the execution.
|
||||
if level == logger.FatalLevel {
|
||||
l.opts.ExitFunc(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *zeroLogger) String() string {
|
||||
return "zerolog"
|
||||
}
|
||||
|
||||
func (l *zeroLogger) Options() logger.Options {
|
||||
// FIXME: How to return full opts?
|
||||
return l.opts.Options
|
||||
}
|
||||
|
||||
// NewLogger builds a new logger based on options.
|
||||
func NewLogger(opts ...logger.Option) logger.Logger {
|
||||
// Default options
|
||||
options := Options{
|
||||
Options: logger.Options{
|
||||
Level: 100,
|
||||
Fields: make(map[string]interface{}),
|
||||
Out: os.Stderr,
|
||||
Context: context.Background(),
|
||||
},
|
||||
ReportCaller: false,
|
||||
UseAsDefault: false,
|
||||
Mode: Production,
|
||||
ExitFunc: os.Exit,
|
||||
}
|
||||
|
||||
l := &zeroLogger{opts: options}
|
||||
_ = l.Init(opts...)
|
||||
return l
|
||||
}
|
||||
|
||||
func loggerToZerologLevel(level logger.Level) zerolog.Level {
|
||||
switch level {
|
||||
case logger.TraceLevel:
|
||||
return zerolog.TraceLevel
|
||||
case logger.DebugLevel:
|
||||
return zerolog.DebugLevel
|
||||
case logger.InfoLevel:
|
||||
return zerolog.InfoLevel
|
||||
case logger.WarnLevel:
|
||||
return zerolog.WarnLevel
|
||||
case logger.ErrorLevel:
|
||||
return zerolog.ErrorLevel
|
||||
case logger.FatalLevel:
|
||||
return zerolog.FatalLevel
|
||||
default:
|
||||
return zerolog.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func ZerologToLoggerLevel(level zerolog.Level) logger.Level {
|
||||
switch level {
|
||||
case zerolog.TraceLevel:
|
||||
return logger.TraceLevel
|
||||
case zerolog.DebugLevel:
|
||||
return logger.DebugLevel
|
||||
case zerolog.InfoLevel:
|
||||
return logger.InfoLevel
|
||||
case zerolog.WarnLevel:
|
||||
return logger.WarnLevel
|
||||
case zerolog.ErrorLevel:
|
||||
return logger.ErrorLevel
|
||||
case zerolog.FatalLevel:
|
||||
return logger.FatalLevel
|
||||
default:
|
||||
return logger.InfoLevel
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
// Package memory provides an in-memory registry
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
sendEventTime = 10 * time.Millisecond
|
||||
ttlPruneTime = time.Second
|
||||
)
|
||||
|
||||
type node struct {
|
||||
*registry.Node
|
||||
TTL time.Duration
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
type record struct {
|
||||
Name string
|
||||
Version string
|
||||
Metadata map[string]string
|
||||
Nodes map[string]*node
|
||||
Endpoints []*registry.Endpoint
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
options registry.Options
|
||||
|
||||
sync.RWMutex
|
||||
records map[string]map[string]*record
|
||||
watchers map[string]*Watcher
|
||||
}
|
||||
|
||||
func NewRegistry(opts ...registry.Option) registry.Registry {
|
||||
options := registry.Options{
|
||||
Context: context.Background(),
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
records := getServiceRecords(options.Context)
|
||||
if records == nil {
|
||||
records = make(map[string]map[string]*record)
|
||||
}
|
||||
|
||||
reg := &Registry{
|
||||
options: options,
|
||||
records: records,
|
||||
watchers: make(map[string]*Watcher),
|
||||
}
|
||||
|
||||
go reg.ttlPrune()
|
||||
|
||||
return reg
|
||||
}
|
||||
|
||||
func (m *Registry) ttlPrune() {
|
||||
prune := time.NewTicker(ttlPruneTime)
|
||||
defer prune.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-prune.C:
|
||||
m.Lock()
|
||||
for name, records := range m.records {
|
||||
for version, record := range records {
|
||||
for id, n := range record.Nodes {
|
||||
if n.TTL != 0 && time.Since(n.LastSeen) > n.TTL {
|
||||
m.options.Logger.Logf(logger.DebugLevel, "Registry TTL expired for node %s of service %s", n.Id, name)
|
||||
delete(m.records[name][version].Nodes, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) sendEvent(r *registry.Result) {
|
||||
m.RLock()
|
||||
watchers := make([]*Watcher, 0, len(m.watchers))
|
||||
for _, w := range m.watchers {
|
||||
watchers = append(watchers, w)
|
||||
}
|
||||
m.RUnlock()
|
||||
|
||||
for _, w := range watchers {
|
||||
select {
|
||||
case <-w.exit:
|
||||
m.Lock()
|
||||
delete(m.watchers, w.id)
|
||||
m.Unlock()
|
||||
default:
|
||||
select {
|
||||
case w.res <- r:
|
||||
case <-time.After(sendEventTime):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) Init(opts ...registry.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&m.options)
|
||||
}
|
||||
|
||||
// add services
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
records := getServiceRecords(m.options.Context)
|
||||
for name, record := range records {
|
||||
// add a whole new service including all of its versions
|
||||
if _, ok := m.records[name]; !ok {
|
||||
m.records[name] = record
|
||||
continue
|
||||
}
|
||||
// add the versions of the service we dont track yet
|
||||
for version, r := range record {
|
||||
if _, ok := m.records[name][version]; !ok {
|
||||
m.records[name][version] = r
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Registry) Options() registry.Options {
|
||||
return m.options
|
||||
}
|
||||
|
||||
func (m *Registry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
log := m.options.Logger
|
||||
|
||||
var options registry.RegisterOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
r := serviceToRecord(s, options.TTL)
|
||||
|
||||
if _, ok := m.records[s.Name]; !ok {
|
||||
m.records[s.Name] = make(map[string]*record)
|
||||
}
|
||||
|
||||
if _, ok := m.records[s.Name][s.Version]; !ok {
|
||||
m.records[s.Name][s.Version] = r
|
||||
log.Logf(logger.DebugLevel, "Registry added new service: %s, version: %s", s.Name, s.Version)
|
||||
go m.sendEvent(®istry.Result{Action: "update", Service: s})
|
||||
return nil
|
||||
}
|
||||
|
||||
addedNodes := false
|
||||
for _, n := range s.Nodes {
|
||||
if _, ok := m.records[s.Name][s.Version].Nodes[n.Id]; !ok {
|
||||
addedNodes = true
|
||||
metadata := make(map[string]string)
|
||||
for k, v := range n.Metadata {
|
||||
metadata[k] = v
|
||||
m.records[s.Name][s.Version].Nodes[n.Id] = &node{
|
||||
Node: ®istry.Node{
|
||||
Id: n.Id,
|
||||
Address: n.Address,
|
||||
Metadata: metadata,
|
||||
},
|
||||
TTL: options.TTL,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if addedNodes {
|
||||
log.Logf(logger.DebugLevel, "Registry added new node to service: %s, version: %s", s.Name, s.Version)
|
||||
go m.sendEvent(®istry.Result{Action: "update", Service: s})
|
||||
return nil
|
||||
}
|
||||
|
||||
// refresh TTL and timestamp
|
||||
for _, n := range s.Nodes {
|
||||
log.Logf(logger.DebugLevel, "Updated registration for service: %s, version: %s", s.Name, s.Version)
|
||||
m.records[s.Name][s.Version].Nodes[n.Id].TTL = options.TTL
|
||||
m.records[s.Name][s.Version].Nodes[n.Id].LastSeen = time.Now()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Registry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
log := m.options.Logger
|
||||
|
||||
if _, ok := m.records[s.Name]; ok {
|
||||
if _, ok := m.records[s.Name][s.Version]; ok {
|
||||
for _, n := range s.Nodes {
|
||||
if _, ok := m.records[s.Name][s.Version].Nodes[n.Id]; ok {
|
||||
log.Logf(logger.DebugLevel, "Registry removed node from service: %s, version: %s", s.Name, s.Version)
|
||||
delete(m.records[s.Name][s.Version].Nodes, n.Id)
|
||||
}
|
||||
}
|
||||
if len(m.records[s.Name][s.Version].Nodes) == 0 {
|
||||
delete(m.records[s.Name], s.Version)
|
||||
log.Logf(logger.DebugLevel, "Registry removed service: %s, version: %s", s.Name, s.Version)
|
||||
}
|
||||
}
|
||||
if len(m.records[s.Name]) == 0 {
|
||||
delete(m.records, s.Name)
|
||||
log.Logf(logger.DebugLevel, "Registry removed service: %s", s.Name)
|
||||
}
|
||||
go m.sendEvent(®istry.Result{Action: "delete", Service: s})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Registry) GetService(name string, opts ...registry.GetOption) ([]*registry.Service, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
records, ok := m.records[name]
|
||||
if !ok {
|
||||
return nil, registry.ErrNotFound
|
||||
}
|
||||
|
||||
services := make([]*registry.Service, len(m.records[name]))
|
||||
i := 0
|
||||
for _, record := range records {
|
||||
services[i] = recordToService(record)
|
||||
i++
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func (m *Registry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
var services []*registry.Service
|
||||
for _, records := range m.records {
|
||||
for _, record := range records {
|
||||
services = append(services, recordToService(record))
|
||||
}
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func (m *Registry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
|
||||
var wo registry.WatchOptions
|
||||
for _, o := range opts {
|
||||
o(&wo)
|
||||
}
|
||||
|
||||
w := &Watcher{
|
||||
exit: make(chan bool),
|
||||
res: make(chan *registry.Result),
|
||||
id: uuid.New().String(),
|
||||
wo: wo,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
m.watchers[w.id] = w
|
||||
m.Unlock()
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (m *Registry) String() string {
|
||||
return "memory"
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
type memoryWatcher struct {
|
||||
exit chan bool
|
||||
opts registry.WatchOptions
|
||||
}
|
||||
|
||||
func (m *memoryWatcher) Next() (*registry.Result, error) {
|
||||
// not implement so we just block until exit
|
||||
<-m.exit
|
||||
return nil, errors.New("watcher stopped")
|
||||
}
|
||||
|
||||
func (m *memoryWatcher) Stop() {
|
||||
select {
|
||||
case <-m.exit:
|
||||
return
|
||||
default:
|
||||
close(m.exit)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
type servicesKey struct{}
|
||||
|
||||
func getServiceRecords(ctx context.Context) map[string]map[string]*record {
|
||||
memServices, ok := ctx.Value(servicesKey{}).(map[string][]*registry.Service)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
services := make(map[string]map[string]*record)
|
||||
|
||||
for name, svc := range memServices {
|
||||
if _, ok := services[name]; !ok {
|
||||
services[name] = make(map[string]*record)
|
||||
}
|
||||
// go through every version of the service
|
||||
for _, s := range svc {
|
||||
services[s.Name][s.Version] = serviceToRecord(s, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// Services is an option that preloads service data.
|
||||
func Services(s map[string][]*registry.Service) registry.Option {
|
||||
return func(o *registry.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, servicesKey{}, s)
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
func serviceToRecord(s *registry.Service, ttl time.Duration) *record {
|
||||
metadata := make(map[string]string, len(s.Metadata))
|
||||
for k, v := range s.Metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
|
||||
nodes := make(map[string]*node, len(s.Nodes))
|
||||
for _, n := range s.Nodes {
|
||||
nodes[n.Id] = &node{
|
||||
Node: n,
|
||||
TTL: ttl,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
endpoints := make([]*registry.Endpoint, len(s.Endpoints))
|
||||
for i, e := range s.Endpoints {
|
||||
endpoints[i] = e
|
||||
}
|
||||
|
||||
return &record{
|
||||
Name: s.Name,
|
||||
Version: s.Version,
|
||||
Metadata: metadata,
|
||||
Nodes: nodes,
|
||||
Endpoints: endpoints,
|
||||
}
|
||||
}
|
||||
|
||||
func recordToService(r *record) *registry.Service {
|
||||
metadata := make(map[string]string, len(r.Metadata))
|
||||
for k, v := range r.Metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
|
||||
endpoints := make([]*registry.Endpoint, len(r.Endpoints))
|
||||
for i, e := range r.Endpoints {
|
||||
request := new(registry.Value)
|
||||
if e.Request != nil {
|
||||
*request = *e.Request
|
||||
}
|
||||
response := new(registry.Value)
|
||||
if e.Response != nil {
|
||||
*response = *e.Response
|
||||
}
|
||||
|
||||
metadata := make(map[string]string, len(e.Metadata))
|
||||
for k, v := range e.Metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
|
||||
endpoints[i] = ®istry.Endpoint{
|
||||
Name: e.Name,
|
||||
Request: request,
|
||||
Response: response,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
nodes := make([]*registry.Node, len(r.Nodes))
|
||||
i := 0
|
||||
for _, n := range r.Nodes {
|
||||
metadata := make(map[string]string, len(n.Metadata))
|
||||
for k, v := range n.Metadata {
|
||||
metadata[k] = v
|
||||
}
|
||||
|
||||
nodes[i] = ®istry.Node{
|
||||
Id: n.Id,
|
||||
Address: n.Address,
|
||||
Metadata: metadata,
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return ®istry.Service{
|
||||
Name: r.Name,
|
||||
Version: r.Version,
|
||||
Metadata: metadata,
|
||||
Endpoints: endpoints,
|
||||
Nodes: nodes,
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
type Watcher struct {
|
||||
id string
|
||||
wo registry.WatchOptions
|
||||
res chan *registry.Result
|
||||
exit chan bool
|
||||
}
|
||||
|
||||
func (m *Watcher) Next() (*registry.Result, error) {
|
||||
for {
|
||||
select {
|
||||
case r := <-m.res:
|
||||
if len(m.wo.Service) > 0 && m.wo.Service != r.Service.Name {
|
||||
continue
|
||||
}
|
||||
return r, nil
|
||||
case <-m.exit:
|
||||
return nil, errors.New("watcher stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Watcher) Stop() {
|
||||
select {
|
||||
case <-m.exit:
|
||||
return
|
||||
default:
|
||||
close(m.exit)
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# GRPC Server
|
||||
|
||||
The grpc server is a [micro.Server](https://pkg.go.dev/github.com/asim/go-micro/server#Server) compatible server.
|
||||
|
||||
## Overview
|
||||
|
||||
The server makes use of the [google.golang.org/grpc](google.golang.org/grpc) framework for the underlying server
|
||||
but continues to use micro handler signatures and protoc-gen-micro generated code.
|
||||
|
||||
## Usage
|
||||
|
||||
Specify the server to your micro service
|
||||
|
||||
```go
|
||||
import (
|
||||
"go-micro.dev/v4"
|
||||
"github.com/go-micro/plugins/v4/server/grpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService(
|
||||
// This needs to be first as it replaces the underlying server
|
||||
// which causes any configuration set before it
|
||||
// to be discarded
|
||||
micro.Server(grpc.NewServer()),
|
||||
micro.Name("greeter"),
|
||||
)
|
||||
}
|
||||
```
|
||||
**NOTE**: Setting the gRPC server and/or client causes the underlying the server/client to be replaced which causes any previous configuration set on that server/client to be discarded. It is therefore recommended to set gRPC server/client before any other configuration
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
b "bytes"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/codec/bytes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/encoding"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type jsonCodec struct{}
|
||||
type bytesCodec struct{}
|
||||
type protoCodec struct{}
|
||||
type wrapCodec struct{ encoding.Codec }
|
||||
|
||||
var jsonpbMarshaler = &jsonpb.Marshaler{
|
||||
EnumsAsInts: false,
|
||||
EmitDefaults: false,
|
||||
OrigName: true,
|
||||
}
|
||||
|
||||
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.Codec.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 {
|
||||
s, err := jsonpbMarshaler.MarshalToString(pb)
|
||||
return []byte(s), err
|
||||
}
|
||||
|
||||
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 jsonpb.Unmarshal(b.NewReader(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"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/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)
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v4/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
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
)
|
||||
|
||||
func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
if d == 3 {
|
||||
return nil
|
||||
}
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Ptr {
|
||||
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.Ptr {
|
||||
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)
|
||||
}
|
||||
+1039
File diff suppressed because it is too large
Load Diff
+66
@@ -0,0 +1,66 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/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
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
"go-micro.dev/v4/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{}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/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
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/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)
|
||||
}
|
||||
+182
@@ -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/v4/logger"
|
||||
"go-micro.dev/v4/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.Ptr {
|
||||
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.Ptr {
|
||||
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)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/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)
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/errors"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/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.Ptr {
|
||||
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
|
||||
}
|
||||
+49
@@ -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
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# HTTP Server
|
||||
|
||||
The HTTP Server is a go-micro.Server. It's a partial implementation which strips out codecs, transports, etc but enables you
|
||||
to create a HTTP Server that could potentially be used for REST based API services.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/micro/go-micro/server"
|
||||
httpServer "github.com/go-micro/plugins/v4/server/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
srv := httpServer.NewServer(
|
||||
server.Name("helloworld"),
|
||||
)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`hello world`))
|
||||
})
|
||||
|
||||
hd := srv.NewHandler(mux)
|
||||
|
||||
srv.Handle(hd)
|
||||
srv.Start()
|
||||
srv.Register()
|
||||
}
|
||||
```
|
||||
|
||||
Or as part of a service
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-micro.dev/v4"
|
||||
"github.com/micro/go-micro/server"
|
||||
httpServer "github.com/go-micro/plugins/v4/server/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
srv := httpServer.NewServer(
|
||||
server.Name("helloworld"),
|
||||
)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`hello world`))
|
||||
})
|
||||
|
||||
hd := srv.NewHandler(mux)
|
||||
|
||||
srv.Handle(hd)
|
||||
|
||||
service := micro.NewService(
|
||||
micro.Server(srv),
|
||||
)
|
||||
service.Init()
|
||||
service.Run()
|
||||
}
|
||||
```
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type buffer struct {
|
||||
*bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *buffer) Close() error {
|
||||
b.Buffer.Reset()
|
||||
return nil
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/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)
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
"go-micro.dev/v4/util/addr"
|
||||
)
|
||||
|
||||
func serviceDef(opts server.Options) *registry.Service {
|
||||
var advt, host string
|
||||
var port int
|
||||
|
||||
if len(opts.Advertise) > 0 {
|
||||
advt = opts.Advertise
|
||||
} else {
|
||||
advt = opts.Address
|
||||
}
|
||||
|
||||
parts := strings.Split(advt, ":")
|
||||
if len(parts) > 1 {
|
||||
host = strings.Join(parts[:len(parts)-1], ":")
|
||||
port, _ = strconv.Atoi(parts[len(parts)-1])
|
||||
} else {
|
||||
host = parts[0]
|
||||
}
|
||||
|
||||
addr, err := addr.Extract(host)
|
||||
if err != nil {
|
||||
addr = host
|
||||
}
|
||||
|
||||
node := ®istry.Node{
|
||||
Id: opts.Name + "-" + opts.Id,
|
||||
Address: net.JoinHostPort(addr, fmt.Sprint(port)),
|
||||
Metadata: opts.Metadata,
|
||||
}
|
||||
|
||||
node.Metadata["server"] = "http"
|
||||
node.Metadata["broker"] = opts.Broker.String()
|
||||
node.Metadata["registry"] = opts.Registry.String()
|
||||
node.Metadata["protocol"] = "http"
|
||||
|
||||
return ®istry.Service{
|
||||
Name: opts.Name,
|
||||
Version: opts.Version,
|
||||
Nodes: []*registry.Node{node},
|
||||
}
|
||||
}
|
||||
|
||||
func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
if d == 3 {
|
||||
return nil
|
||||
}
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Ptr {
|
||||
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, ",")
|
||||
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.Ptr {
|
||||
p = p.Elem()
|
||||
}
|
||||
arg.Type = "[]" + p.Name()
|
||||
val := extractValue(v.Elem(), d+1)
|
||||
if val != nil {
|
||||
arg.Values = append(arg.Values, val)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return ®istry.Endpoint{
|
||||
Name: method.Name,
|
||||
Request: request,
|
||||
Response: response,
|
||||
Metadata: map[string]string{
|
||||
"stream": fmt.Sprintf("%v", stream),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
)
|
||||
|
||||
type httpHandler struct {
|
||||
opts server.HandlerOptions
|
||||
eps []*registry.Endpoint
|
||||
hd interface{}
|
||||
}
|
||||
|
||||
func (h *httpHandler) Name() string {
|
||||
return "handler"
|
||||
}
|
||||
|
||||
func (h *httpHandler) Handler() interface{} {
|
||||
return h.hd
|
||||
}
|
||||
|
||||
func (h *httpHandler) Endpoints() []*registry.Endpoint {
|
||||
return h.eps
|
||||
}
|
||||
|
||||
func (h *httpHandler) Options() server.HandlerOptions {
|
||||
return h.opts
|
||||
}
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
// Package http implements a go-micro.Server
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/codec/jsonrpc"
|
||||
"go-micro.dev/v4/codec/protorpc"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
"go-micro.dev/v4/util/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
sync.Mutex
|
||||
opts server.Options
|
||||
hd server.Handler
|
||||
exit chan chan error
|
||||
registerOnce sync.Once
|
||||
subscribers map[*httpSubscriber][]broker.Subscriber
|
||||
// used for first registration
|
||||
registered bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultServers["http"] = NewServer
|
||||
}
|
||||
|
||||
func (h *httpServer) newCodec(contentType string) (codec.NewCodec, error) {
|
||||
if cf, ok := h.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)
|
||||
}
|
||||
|
||||
func (h *httpServer) getListener() net.Listener {
|
||||
if h.opts.Context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if l, ok := h.opts.Context.Value(netListener{}).(net.Listener); ok && l != nil {
|
||||
return l
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) Options() server.Options {
|
||||
h.Lock()
|
||||
opts := h.opts
|
||||
h.Unlock()
|
||||
return opts
|
||||
}
|
||||
|
||||
func (h *httpServer) Init(opts ...server.Option) error {
|
||||
h.Lock()
|
||||
for _, o := range opts {
|
||||
o(&h.opts)
|
||||
}
|
||||
h.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) Handle(handler server.Handler) error {
|
||||
if _, ok := handler.Handler().(http.Handler); !ok {
|
||||
return errors.New("Handle requires http.Handler")
|
||||
}
|
||||
h.Lock()
|
||||
h.hd = handler
|
||||
h.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) NewHandler(handler interface{}, opts ...server.HandlerOption) server.Handler {
|
||||
options := server.HandlerOptions{
|
||||
Metadata: make(map[string]map[string]string),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var eps []*registry.Endpoint
|
||||
|
||||
if !options.Internal {
|
||||
for name, metadata := range options.Metadata {
|
||||
eps = append(eps, ®istry.Endpoint{
|
||||
Name: name,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &httpHandler{
|
||||
eps: eps,
|
||||
hd: handler,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpServer) NewSubscriber(topic string, handler interface{}, opts ...server.SubscriberOption) server.Subscriber {
|
||||
return newSubscriber(topic, handler, opts...)
|
||||
}
|
||||
|
||||
func (h *httpServer) Subscribe(sb server.Subscriber) error {
|
||||
sub, ok := sb.(*httpSubscriber)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid subscriber: expected *httpSubscriber")
|
||||
}
|
||||
if len(sub.handlers) == 0 {
|
||||
return fmt.Errorf("invalid subscriber: no handler functions")
|
||||
}
|
||||
|
||||
if err := validateSubscriber(sb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
_, ok = h.subscribers[sub]
|
||||
if ok {
|
||||
return fmt.Errorf("subscriber %v already exists", h)
|
||||
}
|
||||
h.subscribers[sub] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) Register() error {
|
||||
h.Lock()
|
||||
opts := h.opts
|
||||
eps := h.hd.Endpoints()
|
||||
h.Unlock()
|
||||
|
||||
service := serviceDef(opts)
|
||||
service.Endpoints = eps
|
||||
|
||||
h.Lock()
|
||||
var subscriberList []*httpSubscriber
|
||||
for e := range h.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
|
||||
})
|
||||
for _, e := range subscriberList {
|
||||
service.Endpoints = append(service.Endpoints, e.Endpoints()...)
|
||||
}
|
||||
h.Unlock()
|
||||
|
||||
rOpts := []registry.RegisterOption{
|
||||
registry.RegisterTTL(opts.RegisterTTL),
|
||||
}
|
||||
|
||||
h.registerOnce.Do(func() {
|
||||
opts.Logger.Logf(logger.InfoLevel, "Registering node: %s", opts.Name+"-"+opts.Id)
|
||||
})
|
||||
|
||||
if err := opts.Registry.Register(service, rOpts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if h.registered {
|
||||
return nil
|
||||
}
|
||||
h.registered = true
|
||||
|
||||
for sb := range h.subscribers {
|
||||
handler := h.createSubHandler(sb, opts)
|
||||
var subOpts []broker.SubscribeOption
|
||||
if queue := sb.Options().Queue; len(queue) > 0 {
|
||||
subOpts = append(subOpts, broker.Queue(queue))
|
||||
}
|
||||
|
||||
if cx := sb.Options().Context; cx != nil {
|
||||
subOpts = append(subOpts, broker.SubscribeContext(cx))
|
||||
}
|
||||
|
||||
if !sb.Options().AutoAck {
|
||||
subOpts = append(subOpts, broker.DisableAutoAck())
|
||||
}
|
||||
|
||||
sub, err := opts.Broker.Subscribe(sb.Topic(), handler, subOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.subscribers[sb] = []broker.Subscriber{sub}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) Deregister() error {
|
||||
h.Lock()
|
||||
opts := h.opts
|
||||
h.Unlock()
|
||||
|
||||
opts.Logger.Logf(logger.InfoLevel, "Deregistering node: %s", opts.Name+"-"+opts.Id)
|
||||
|
||||
service := serviceDef(opts)
|
||||
if err := opts.Registry.Deregister(service); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Lock()
|
||||
|
||||
if !h.registered {
|
||||
h.Unlock()
|
||||
return nil
|
||||
}
|
||||
h.registered = false
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for sb, subs := range h.subscribers {
|
||||
for _, sub := range subs {
|
||||
wg.Add(1)
|
||||
go func(s broker.Subscriber) {
|
||||
defer wg.Done()
|
||||
opts.Logger.Logf(logger.InfoLevel, "Unsubscribing from topic: %s", s.Topic())
|
||||
s.Unsubscribe()
|
||||
}(sub)
|
||||
}
|
||||
h.subscribers[sb] = nil
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
h.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) Start() error {
|
||||
h.Lock()
|
||||
opts := h.opts
|
||||
hd := h.hd
|
||||
h.Unlock()
|
||||
|
||||
var (
|
||||
ln net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
if l := h.getListener(); l != nil {
|
||||
ln = l
|
||||
} else if opts.TLSConfig != nil {
|
||||
ln, err = tls.Listen("tcp", opts.Address, opts.TLSConfig)
|
||||
} else {
|
||||
ln, err = net.Listen("tcp", opts.Address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.Logger.Logf(logger.InfoLevel, "Listening on %s", ln.Addr().String())
|
||||
|
||||
h.Lock()
|
||||
h.opts.Address = ln.Addr().String()
|
||||
h.Unlock()
|
||||
|
||||
handler, ok := hd.Handler().(http.Handler)
|
||||
if !ok {
|
||||
return errors.New("Server required http.Handler")
|
||||
}
|
||||
|
||||
if err = opts.Broker.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// register
|
||||
if err = h.Register(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go http.Serve(ln, handler)
|
||||
|
||||
go func() {
|
||||
t := new(time.Ticker)
|
||||
|
||||
// only process if it exists
|
||||
if opts.RegisterInterval > time.Duration(0) {
|
||||
// new ticker
|
||||
t = time.NewTicker(opts.RegisterInterval)
|
||||
}
|
||||
|
||||
// return error chan
|
||||
var ch chan error
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
// register self on interval
|
||||
case <-t.C:
|
||||
if err := h.Register(); err != nil {
|
||||
opts.Logger.Log(logger.ErrorLevel, "Server register error: ", err)
|
||||
}
|
||||
// wait for exit
|
||||
case ch = <-h.exit:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
|
||||
// deregister
|
||||
h.Deregister()
|
||||
|
||||
opts.Broker.Disconnect()
|
||||
|
||||
// Solve the problem of early exit
|
||||
ch <- ln.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpServer) Stop() error {
|
||||
ch := make(chan error)
|
||||
h.exit <- ch
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (h *httpServer) String() string {
|
||||
if h.opts.TLSConfig != nil {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func newServer(opts ...server.Option) server.Server {
|
||||
return &httpServer{
|
||||
opts: newOptions(opts...),
|
||||
exit: make(chan chan error),
|
||||
subscribers: make(map[*httpSubscriber][]broker.Subscriber),
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(opts ...server.Option) server.Server {
|
||||
return newServer(opts...)
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package http
|
||||
|
||||
import "go-micro.dev/v4/codec"
|
||||
|
||||
type httpMessage struct {
|
||||
topic string
|
||||
payload interface{}
|
||||
contentType string
|
||||
header map[string]string
|
||||
body []byte
|
||||
codec codec.Reader
|
||||
}
|
||||
|
||||
func (r *httpMessage) Topic() string {
|
||||
return r.topic
|
||||
}
|
||||
|
||||
func (r *httpMessage) Payload() interface{} {
|
||||
return r.payload
|
||||
}
|
||||
|
||||
func (r *httpMessage) ContentType() string {
|
||||
return r.contentType
|
||||
}
|
||||
|
||||
func (r *httpMessage) Header() map[string]string {
|
||||
return r.header
|
||||
}
|
||||
|
||||
func (r *httpMessage) Body() []byte {
|
||||
return r.body
|
||||
}
|
||||
|
||||
func (r *httpMessage) Codec() codec.Reader {
|
||||
return r.codec
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
)
|
||||
|
||||
type netListener struct{}
|
||||
|
||||
func newOptions(opt ...server.Option) server.Options {
|
||||
opts := server.Options{
|
||||
Codecs: make(map[string]codec.NewCodec),
|
||||
Metadata: map[string]string{},
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = logger.DefaultLogger
|
||||
}
|
||||
|
||||
if opts.Broker == nil {
|
||||
opts.Broker = broker.DefaultBroker
|
||||
}
|
||||
|
||||
if opts.Registry == nil {
|
||||
opts.Registry = registry.DefaultRegistry
|
||||
}
|
||||
|
||||
if len(opts.Address) == 0 {
|
||||
opts.Address = server.DefaultAddress
|
||||
}
|
||||
|
||||
if len(opts.Name) == 0 {
|
||||
opts.Name = server.DefaultName
|
||||
}
|
||||
|
||||
if len(opts.Id) == 0 {
|
||||
opts.Id = server.DefaultId
|
||||
}
|
||||
|
||||
if len(opts.Version) == 0 {
|
||||
opts.Version = server.DefaultVersion
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// Listener specifies the net.Listener to use instead of the default.
|
||||
func Listener(l net.Listener) server.Option {
|
||||
return setServerOption(netListener{}, l)
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"go-micro.dev/v4/broker"
|
||||
"go-micro.dev/v4/codec"
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
)
|
||||
|
||||
const (
|
||||
subSig = "func(context.Context, interface{}) error"
|
||||
)
|
||||
|
||||
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
type handler struct {
|
||||
method reflect.Value
|
||||
reqType reflect.Type
|
||||
ctxType reflect.Type
|
||||
}
|
||||
|
||||
type httpSubscriber struct {
|
||||
topic string
|
||||
rcvr reflect.Value
|
||||
typ reflect.Type
|
||||
subscriber interface{}
|
||||
handlers []*handler
|
||||
endpoints []*registry.Endpoint
|
||||
opts server.SubscriberOptions
|
||||
}
|
||||
|
||||
// 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.Ptr {
|
||||
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() == ""
|
||||
}
|
||||
|
||||
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 &httpSubscriber{
|
||||
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 (s *httpServer) createSubHandler(sb *httpSubscriber, opts server.Options) broker.Handler {
|
||||
return func(p broker.Event) error {
|
||||
msg := p.Message()
|
||||
ct := msg.Header["Content-Type"]
|
||||
cf, err := s.newCodec(ct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdr := make(map[string]string)
|
||||
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.Ptr {
|
||||
req = reflect.New(handler.reqType.Elem())
|
||||
} else {
|
||||
req = reflect.New(handler.reqType)
|
||||
isVal = true
|
||||
}
|
||||
if isVal {
|
||||
req = req.Elem()
|
||||
}
|
||||
|
||||
b := &buffer{bytes.NewBuffer(msg.Body)}
|
||||
co := cf(b)
|
||||
defer co.Close()
|
||||
|
||||
if err := co.ReadHeader(&codec.Message{}, codec.Event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := co.ReadBody(req.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn := func(ctx context.Context, msg 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 err := returnValues[0].Interface(); err != nil {
|
||||
return err.(error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := len(opts.SubWrappers); i > 0; i-- {
|
||||
fn = opts.SubWrappers[i-1](fn)
|
||||
}
|
||||
|
||||
go func() {
|
||||
results <- fn(ctx, &httpMessage{
|
||||
topic: sb.topic,
|
||||
contentType: ct,
|
||||
payload: req.Interface(),
|
||||
header: msg.Header,
|
||||
body: msg.Body,
|
||||
codec: co,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
var errors []string
|
||||
|
||||
for i := 0; i < len(sb.handlers); i++ {
|
||||
if err := <-results; err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpSubscriber) Topic() string {
|
||||
return s.topic
|
||||
}
|
||||
|
||||
func (s *httpSubscriber) Subscriber() interface{} {
|
||||
return s.subscriber
|
||||
}
|
||||
|
||||
func (s *httpSubscriber) Endpoints() []*registry.Endpoint {
|
||||
return s.endpoints
|
||||
}
|
||||
|
||||
func (s *httpSubscriber) Options() server.SubscriberOptions {
|
||||
return s.opts
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# NATS JetStream Key Value Store Plugin
|
||||
|
||||
This plugin uses the NATS JetStream [KeyValue Store](https://docs.nats.io/nats-concepts/jetstream/key-value-store) to implement the Go-Micro store interface.
|
||||
|
||||
You can use this plugin like any other store plugin.
|
||||
To start a local NATS JetStream server run `nats-server -js`.
|
||||
|
||||
To manually create a new storage object call:
|
||||
|
||||
```go
|
||||
natsjskv.NewStore(opts ...store.Option)
|
||||
```
|
||||
|
||||
The Go-Micro store interface uses databases and tables to store keys. These translate
|
||||
to buckets (key value stores) and key prefixes. If no database (bucket name) is provided, "default" will be used.
|
||||
|
||||
You can call `Write` with any arbitrary database name, and if a bucket with that name does not exist yet,
|
||||
it will be automatically created.
|
||||
|
||||
If a table name is provided, it will use it to prefix the key as `<table>_<key>`.
|
||||
|
||||
To delete a bucket, and all the key/value pairs in it, pass the `DeleteBucket` option to the `Delete`
|
||||
method, then they key name will be interpreted as a bucket name, and the bucket will be deleted.
|
||||
|
||||
Next to the default store options, a few NATS specific options are available:
|
||||
|
||||
|
||||
```go
|
||||
// NatsOptions accepts nats.Options
|
||||
NatsOptions(opts nats.Options)
|
||||
|
||||
// JetStreamOptions accepts multiple nats.JSOpt
|
||||
JetStreamOptions(opts ...nats.JSOpt)
|
||||
|
||||
// KeyValueOptions accepts multiple nats.KeyValueConfig
|
||||
// This will create buckets with the provided configs at initialization.
|
||||
//
|
||||
// type KeyValueConfig struct {
|
||||
// Bucket string
|
||||
// Description string
|
||||
// MaxValueSize int32
|
||||
// History uint8
|
||||
// TTL time.Duration
|
||||
// MaxBytes int64
|
||||
// Storage StorageType
|
||||
// Replicas int
|
||||
// Placement *Placement
|
||||
// RePublish *RePublish
|
||||
// Mirror *StreamSource
|
||||
// Sources []*StreamSource
|
||||
}
|
||||
KeyValueOptions(cfg ...*nats.KeyValueConfig)
|
||||
|
||||
// DefaultTTL sets the default TTL to use for new buckets
|
||||
// By default no TTL is set.
|
||||
//
|
||||
// TTL ON INDIVIDUAL WRITE CALLS IS NOT SUPPORTED, only bucket wide TTL.
|
||||
// Either set a default TTL with this option or provide bucket specific options
|
||||
// with ObjectStoreOptions
|
||||
DefaultTTL(ttl time.Duration)
|
||||
|
||||
// DefaultMemory sets the default storage type to memory only.
|
||||
//
|
||||
// The default is file storage, persisting storage between service restarts.
|
||||
// Be aware that the default storage location of NATS the /tmp dir is, and thus
|
||||
// won't persist reboots.
|
||||
DefaultMemory()
|
||||
|
||||
// DefaultDescription sets the default description to use when creating new
|
||||
// buckets. The default is "Store managed by go-micro"
|
||||
DefaultDescription(text string)
|
||||
|
||||
// DeleteBucket will use the key passed to Delete as a bucket (database) name,
|
||||
// and delete the bucket.
|
||||
// This option should not be combined with the store.DeleteFrom option, as
|
||||
// that will overwrite the delete action.
|
||||
DeleteBucket()
|
||||
```
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package natsjskv
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// setStoreOption returns a function to setup a context with given value.
|
||||
func setStoreOption(k, v interface{}) store.Option {
|
||||
return func(o *store.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package natsjskv
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NatsKey is a convenience function to create a key for the nats kv store.
|
||||
func (n *natsStore) NatsKey(table, microkey string) string {
|
||||
return n.NewKey(table, microkey, "").NatsKey()
|
||||
}
|
||||
|
||||
// MicroKey is a convenience function to create a key for the micro interface.
|
||||
func (n *natsStore) MicroKey(table, natskey string) string {
|
||||
return n.NewKey(table, "", natskey).MicroKey()
|
||||
}
|
||||
|
||||
// MicroKeyFilter is a convenience function to create a key for the micro interface.
|
||||
// It returns false if the key does not match the table, prefix or suffix.
|
||||
func (n *natsStore) MicroKeyFilter(table, natskey string, prefix, suffix string) (string, bool) {
|
||||
k := n.NewKey(table, "", natskey)
|
||||
return k.MicroKey(), k.Check(table, prefix, suffix)
|
||||
}
|
||||
|
||||
// Key represents a key in the store.
|
||||
// They are used to convert nats keys (base32 encoded) to micro keys (plain text - no table prefix) and vice versa.
|
||||
type Key struct {
|
||||
// Plain is the plain key as requested by the go-micro interface.
|
||||
Plain string
|
||||
// Full is the full key including the table prefix.
|
||||
Full string
|
||||
// Encoded is the base64 encoded key as used by the nats kv store.
|
||||
Encoded string
|
||||
}
|
||||
|
||||
// NewKey creates a new key. Either plain or encoded must be set.
|
||||
func (n *natsStore) NewKey(table string, plain, encoded string) *Key {
|
||||
k := &Key{
|
||||
Plain: plain,
|
||||
Encoded: encoded,
|
||||
}
|
||||
|
||||
switch {
|
||||
case k.Plain != "":
|
||||
k.Full = getKey(k.Plain, table)
|
||||
k.Encoded = encode(k.Full, n.encoding)
|
||||
case k.Encoded != "":
|
||||
k.Full = decode(k.Encoded, n.encoding)
|
||||
k.Plain = trimKey(k.Full, table)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
// NatsKey returns a key the nats kv store can work with.
|
||||
func (k *Key) NatsKey() string {
|
||||
return k.Encoded
|
||||
}
|
||||
|
||||
// MicroKey returns a key the micro interface can work with.
|
||||
func (k *Key) MicroKey() string {
|
||||
return k.Plain
|
||||
}
|
||||
|
||||
// Check returns false if the key does not match the table, prefix or suffix.
|
||||
func (k *Key) Check(table, prefix, suffix string) bool {
|
||||
if table != "" && k.Full != getKey(k.Plain, table) {
|
||||
return false
|
||||
}
|
||||
|
||||
if prefix != "" && !strings.HasPrefix(k.Plain, prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
if suffix != "" && !strings.HasSuffix(k.Plain, suffix) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func encode(s string, alg string) string {
|
||||
switch alg {
|
||||
case "base32":
|
||||
return base32.StdEncoding.EncodeToString([]byte(s))
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func decode(s string, alg string) string {
|
||||
switch alg {
|
||||
case "base32":
|
||||
b, err := base32.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
|
||||
return string(b)
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(key, table string) string {
|
||||
if table != "" {
|
||||
return table + "_" + key
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func trimKey(key, table string) string {
|
||||
if table != "" {
|
||||
return strings.TrimPrefix(key, table+"_")
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
+544
@@ -0,0 +1,544 @@
|
||||
// Package natsjskv is a go-micro store plugin for NATS JetStream Key-Value store.
|
||||
package natsjskv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cornelk/hashmap"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/pkg/errors"
|
||||
"go-micro.dev/v4/store"
|
||||
"go-micro.dev/v4/util/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBucketNotFound is returned when the requested bucket does not exist.
|
||||
ErrBucketNotFound = errors.New("Bucket (database) not found")
|
||||
)
|
||||
|
||||
// KeyValueEnvelope is the data structure stored in the key value store.
|
||||
type KeyValueEnvelope struct {
|
||||
Key string `json:"key"`
|
||||
Data []byte `json:"data"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
type natsStore struct {
|
||||
sync.Once
|
||||
sync.RWMutex
|
||||
|
||||
encoding string
|
||||
ttl time.Duration
|
||||
storageType nats.StorageType
|
||||
description string
|
||||
|
||||
opts store.Options
|
||||
nopts nats.Options
|
||||
jsopts []nats.JSOpt
|
||||
kvConfigs []*nats.KeyValueConfig
|
||||
|
||||
conn *nats.Conn
|
||||
js nats.JetStreamContext
|
||||
buckets *hashmap.Map[string, nats.KeyValue]
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultStores["natsjskv"] = NewStore
|
||||
}
|
||||
|
||||
// NewStore will create a new NATS JetStream Object Store.
|
||||
func NewStore(opts ...store.Option) store.Store {
|
||||
options := store.Options{
|
||||
Nodes: []string{},
|
||||
Database: "default",
|
||||
Table: "",
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
n := &natsStore{
|
||||
description: "KeyValue storage administered by go-micro store plugin",
|
||||
opts: options,
|
||||
jsopts: []nats.JSOpt{},
|
||||
kvConfigs: []*nats.KeyValueConfig{},
|
||||
buckets: hashmap.New[string, nats.KeyValue](),
|
||||
storageType: nats.FileStorage,
|
||||
}
|
||||
|
||||
n.setOption(opts...)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// Init initializes the store. It must perform any required setup on the
|
||||
// backing storage implementation and check that it is ready for use,
|
||||
// returning any errors.
|
||||
func (n *natsStore) Init(opts ...store.Option) error {
|
||||
n.setOption(opts...)
|
||||
|
||||
// Connect to NATS servers
|
||||
conn, err := n.nopts.Connect()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to connect to NATS Server")
|
||||
}
|
||||
|
||||
// Create JetStream context
|
||||
js, err := conn.JetStream(n.jsopts...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create JetStream context")
|
||||
}
|
||||
|
||||
n.conn = conn
|
||||
n.js = js
|
||||
|
||||
// Create default config if no configs present
|
||||
if len(n.kvConfigs) == 0 {
|
||||
if _, err := n.mustGetBucketByName(n.opts.Database); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create kv store buckets
|
||||
for _, cfg := range n.kvConfigs {
|
||||
if _, err := n.mustGetBucket(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *natsStore) setOption(opts ...store.Option) {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
}
|
||||
|
||||
n.Once.Do(func() {
|
||||
n.nopts = nats.GetDefaultOptions()
|
||||
})
|
||||
|
||||
// Extract options from context
|
||||
if nopts, ok := n.opts.Context.Value(natsOptionsKey{}).(nats.Options); ok {
|
||||
n.nopts = nopts
|
||||
}
|
||||
|
||||
if jsopts, ok := n.opts.Context.Value(jsOptionsKey{}).([]nats.JSOpt); ok {
|
||||
n.jsopts = append(n.jsopts, jsopts...)
|
||||
}
|
||||
|
||||
if cfg, ok := n.opts.Context.Value(kvOptionsKey{}).([]*nats.KeyValueConfig); ok {
|
||||
n.kvConfigs = append(n.kvConfigs, cfg...)
|
||||
}
|
||||
|
||||
if ttl, ok := n.opts.Context.Value(ttlOptionsKey{}).(time.Duration); ok {
|
||||
n.ttl = ttl
|
||||
}
|
||||
|
||||
if sType, ok := n.opts.Context.Value(memoryOptionsKey{}).(nats.StorageType); ok {
|
||||
n.storageType = sType
|
||||
}
|
||||
|
||||
if text, ok := n.opts.Context.Value(descriptionOptionsKey{}).(string); ok {
|
||||
n.description = text
|
||||
}
|
||||
|
||||
if encoding, ok := n.opts.Context.Value(keyEncodeOptionsKey{}).(string); ok {
|
||||
n.encoding = encoding
|
||||
}
|
||||
|
||||
// Assign store option server addresses to nats options
|
||||
if len(n.opts.Nodes) > 0 {
|
||||
n.nopts.Url = ""
|
||||
n.nopts.Servers = n.opts.Nodes
|
||||
}
|
||||
|
||||
if len(n.nopts.Servers) == 0 && n.nopts.Url == "" {
|
||||
n.nopts.Url = nats.DefaultURL
|
||||
}
|
||||
}
|
||||
|
||||
// Options allows you to view the current options.
|
||||
func (n *natsStore) Options() store.Options {
|
||||
return n.opts
|
||||
}
|
||||
|
||||
// Read takes a single key name and optional ReadOptions. It returns matching []*Record or an error.
|
||||
func (n *natsStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
if err := n.initConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := store.ReadOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
bucket, ok := n.buckets.Get(opt.Database)
|
||||
if !ok {
|
||||
return nil, ErrBucketNotFound
|
||||
}
|
||||
|
||||
keys, err := n.natsKeys(bucket, opt.Table, key, opt.Prefix, opt.Suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records := make([]*store.Record, 0, len(keys))
|
||||
|
||||
for _, key := range keys {
|
||||
rec, ok, err := n.getRecord(bucket, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
records = append(records, rec)
|
||||
}
|
||||
}
|
||||
|
||||
return enforceLimits(records, opt.Limit, opt.Offset), nil
|
||||
}
|
||||
|
||||
// Write writes a record to the store, and returns an error if the record was not written.
|
||||
func (n *natsStore) Write(rec *store.Record, opts ...store.WriteOption) error {
|
||||
if err := n.initConn(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := store.WriteOptions{}
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
store, err := n.mustGetBucketByName(opt.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := json.Marshal(KeyValueEnvelope{
|
||||
Key: rec.Key,
|
||||
Data: rec.Value,
|
||||
Metadata: rec.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal object")
|
||||
}
|
||||
|
||||
if _, err := store.Put(n.NatsKey(opt.Table, rec.Key), b); err != nil {
|
||||
return errors.Wrapf(err, "Failed to store data in bucket '%s'", n.NatsKey(opt.Table, rec.Key))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes the record with the corresponding key from the store.
|
||||
func (n *natsStore) Delete(key string, opts ...store.DeleteOption) error {
|
||||
if err := n.initConn(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := store.DeleteOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
if opt.Table == "DELETE_BUCKET" {
|
||||
n.buckets.Del(key)
|
||||
|
||||
if err := n.js.DeleteKeyValue(key); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete bucket")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
store, ok := n.buckets.Get(opt.Database)
|
||||
if !ok {
|
||||
return ErrBucketNotFound
|
||||
}
|
||||
|
||||
if err := store.Delete(n.NatsKey(opt.Table, key)); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete data")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns any keys that match, or an empty list with no error if none matched.
|
||||
func (n *natsStore) List(opts ...store.ListOption) ([]string, error) {
|
||||
if err := n.initConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := store.ListOptions{}
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
store, ok := n.buckets.Get(opt.Database)
|
||||
if !ok {
|
||||
return nil, ErrBucketNotFound
|
||||
}
|
||||
|
||||
keys, err := n.microKeys(store, opt.Table, opt.Prefix, opt.Suffix)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to list keys in bucket")
|
||||
}
|
||||
|
||||
return enforceLimits(keys, opt.Limit, opt.Offset), nil
|
||||
}
|
||||
|
||||
// Close the store.
|
||||
func (n *natsStore) Close() error {
|
||||
n.conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the name of the implementation.
|
||||
func (n *natsStore) String() string {
|
||||
return "NATS JetStream KeyValueStore"
|
||||
}
|
||||
|
||||
// StoreUpdate is the update type for the store.
|
||||
type StoreUpdate struct {
|
||||
Value KeyValueEnvelope
|
||||
Action string
|
||||
}
|
||||
|
||||
// WatchAll exposes the watcher interface from the underlying JetStreamContext.
|
||||
func (n *natsStore) WatchAll(bucket string, opts ...nats.WatchOpt) (<-chan *StoreUpdate, func() error, error) {
|
||||
if bucket == "" {
|
||||
return nil, nil, errors.New("multi bucket watching is not supported")
|
||||
}
|
||||
|
||||
if err := n.initConn(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
b, err := n.js.KeyValue(bucket)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Failed to get bucket")
|
||||
}
|
||||
|
||||
w, err := b.WatchAll(opts...)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Failed to watch bucket")
|
||||
}
|
||||
|
||||
ch := make(chan *StoreUpdate)
|
||||
go func() {
|
||||
for u := range w.Updates() {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var action string
|
||||
var kv KeyValueEnvelope
|
||||
switch u.Operation() {
|
||||
case nats.KeyValuePut:
|
||||
action = "create"
|
||||
if err := json.Unmarshal(u.Value(), &kv); err != nil {
|
||||
continue
|
||||
}
|
||||
case nats.KeyValueDelete:
|
||||
fallthrough
|
||||
case nats.KeyValuePurge:
|
||||
action = "delete"
|
||||
kv = KeyValueEnvelope{
|
||||
Key: n.NewKey(n.opts.Table, "", u.Key()).MicroKey(),
|
||||
}
|
||||
}
|
||||
|
||||
ch <- &StoreUpdate{
|
||||
Value: kv,
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, w.Stop, nil
|
||||
}
|
||||
|
||||
// thread safe way to initialize the connection.
|
||||
func (n *natsStore) initConn() error {
|
||||
if n.hasConn() {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
// check if conn was initialized meanwhile
|
||||
if n.conn != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return n.Init()
|
||||
}
|
||||
|
||||
// thread safe way to check if n is initialized.
|
||||
func (n *natsStore) hasConn() bool {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
return n.conn != nil
|
||||
}
|
||||
|
||||
// mustGetDefaultBucket returns the bucket with the given name creating it with default configuration if needed.
|
||||
func (n *natsStore) mustGetBucketByName(name string) (nats.KeyValue, error) {
|
||||
return n.mustGetBucket(&nats.KeyValueConfig{
|
||||
Bucket: name,
|
||||
Description: n.description,
|
||||
TTL: n.ttl,
|
||||
Storage: n.storageType,
|
||||
})
|
||||
}
|
||||
|
||||
// mustGetBucket creates a new bucket if it does not exist yet.
|
||||
func (n *natsStore) mustGetBucket(kv *nats.KeyValueConfig) (nats.KeyValue, error) {
|
||||
if store, ok := n.buckets.Get(kv.Bucket); ok {
|
||||
return store, nil
|
||||
}
|
||||
|
||||
store, err := n.js.KeyValue(kv.Bucket)
|
||||
if err != nil {
|
||||
if !errors.Is(err, nats.ErrBucketNotFound) {
|
||||
return nil, errors.Wrapf(err, "Failed to get bucket (%s)", kv.Bucket)
|
||||
}
|
||||
|
||||
store, err = n.js.CreateKeyValue(kv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to create bucket (%s)", kv.Bucket)
|
||||
}
|
||||
}
|
||||
|
||||
n.buckets.Set(kv.Bucket, store)
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// getRecord returns the record with the given key from the nats kv store.
|
||||
func (n *natsStore) getRecord(bucket nats.KeyValue, key string) (*store.Record, bool, error) {
|
||||
obj, err := bucket.Get(key)
|
||||
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||
return nil, false, store.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, false, errors.Wrap(err, "Failed to get object from bucket")
|
||||
}
|
||||
|
||||
var kv KeyValueEnvelope
|
||||
if err := json.Unmarshal(obj.Value(), &kv); err != nil {
|
||||
return nil, false, errors.Wrap(err, "Failed to unmarshal object")
|
||||
}
|
||||
|
||||
if obj.Operation() != nats.KeyValuePut {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
return &store.Record{
|
||||
Key: kv.Key,
|
||||
Value: kv.Data,
|
||||
Metadata: kv.Metadata,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (n *natsStore) natsKeys(bucket nats.KeyValue, table, key string, prefix, suffix bool) ([]string, error) {
|
||||
if !suffix && !prefix {
|
||||
return []string{n.NatsKey(table, key)}, nil
|
||||
}
|
||||
|
||||
toS := func(s string, b bool) string {
|
||||
if b {
|
||||
return s
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
keys, _, err := n.getKeys(bucket, table, toS(key, prefix), toS(key, suffix))
|
||||
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (n *natsStore) microKeys(bucket nats.KeyValue, table, prefix, suffix string) ([]string, error) {
|
||||
_, keys, err := n.getKeys(bucket, table, prefix, suffix)
|
||||
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (n *natsStore) getKeys(bucket nats.KeyValue, table string, prefix, suffix string) ([]string, []string, error) {
|
||||
names, err := bucket.Keys(nats.IgnoreDeletes())
|
||||
if errors.Is(err, nats.ErrKeyNotFound) {
|
||||
return []string{}, []string{}, nil
|
||||
} else if err != nil {
|
||||
return []string{}, []string{}, errors.Wrap(err, "Failed to list objects")
|
||||
}
|
||||
|
||||
natsKeys := make([]string, 0, len(names))
|
||||
microKeys := make([]string, 0, len(names))
|
||||
|
||||
for _, k := range names {
|
||||
mkey, ok := n.MicroKeyFilter(table, k, prefix, suffix)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
natsKeys = append(natsKeys, k)
|
||||
microKeys = append(microKeys, mkey)
|
||||
}
|
||||
|
||||
return natsKeys, microKeys, nil
|
||||
}
|
||||
|
||||
// enforces offset and limit without causing a panic.
|
||||
func enforceLimits[V any](recs []V, limit, offset uint) []V {
|
||||
l := uint(len(recs))
|
||||
|
||||
from := offset
|
||||
if from > l {
|
||||
from = l
|
||||
}
|
||||
|
||||
to := l
|
||||
if limit > 0 && offset+limit < l {
|
||||
to = offset + limit
|
||||
}
|
||||
|
||||
return recs[from:to]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package natsjskv
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// store.Option.
|
||||
type natsOptionsKey struct{}
|
||||
type jsOptionsKey struct{}
|
||||
type kvOptionsKey struct{}
|
||||
type ttlOptionsKey struct{}
|
||||
type memoryOptionsKey struct{}
|
||||
type descriptionOptionsKey struct{}
|
||||
type keyEncodeOptionsKey struct{}
|
||||
|
||||
// NatsOptions accepts nats.Options.
|
||||
func NatsOptions(opts nats.Options) store.Option {
|
||||
return setStoreOption(natsOptionsKey{}, opts)
|
||||
}
|
||||
|
||||
// JetStreamOptions accepts multiple nats.JSOpt.
|
||||
func JetStreamOptions(opts ...nats.JSOpt) store.Option {
|
||||
return setStoreOption(jsOptionsKey{}, opts)
|
||||
}
|
||||
|
||||
// KeyValueOptions accepts multiple nats.KeyValueConfig
|
||||
// This will create buckets with the provided configs at initialization.
|
||||
func KeyValueOptions(cfg ...*nats.KeyValueConfig) store.Option {
|
||||
return setStoreOption(kvOptionsKey{}, cfg)
|
||||
}
|
||||
|
||||
// DefaultTTL sets the default TTL to use for new buckets
|
||||
//
|
||||
// By default no TTL is set.
|
||||
//
|
||||
// TTL ON INDIVIDUAL WRITE CALLS IS NOT SUPPORTED, only bucket wide TTL.
|
||||
// Either set a default TTL with this option or provide bucket specific options
|
||||
//
|
||||
// with ObjectStoreOptions
|
||||
func DefaultTTL(ttl time.Duration) store.Option {
|
||||
return setStoreOption(ttlOptionsKey{}, ttl)
|
||||
}
|
||||
|
||||
// DefaultMemory sets the default storage type to memory only.
|
||||
//
|
||||
// The default is file storage, persisting storage between service restarts.
|
||||
//
|
||||
// Be aware that the default storage location of NATS the /tmp dir is, and thus
|
||||
//
|
||||
// won't persist reboots.
|
||||
func DefaultMemory() store.Option {
|
||||
return setStoreOption(memoryOptionsKey{}, nats.MemoryStorage)
|
||||
}
|
||||
|
||||
// DefaultDescription sets the default description to use when creating new
|
||||
//
|
||||
// buckets. The default is "Store managed by go-micro"
|
||||
func DefaultDescription(text string) store.Option {
|
||||
return setStoreOption(descriptionOptionsKey{}, text)
|
||||
}
|
||||
|
||||
// EncodeKeys will "base32" encode the keys.
|
||||
// This is to work around limited characters usable as keys for the natsjs kv store.
|
||||
// See details here: https://docs.nats.io/nats-concepts/subjects#characters-allowed-for-subject-names
|
||||
func EncodeKeys() store.Option {
|
||||
return setStoreOption(keyEncodeOptionsKey{}, "base32")
|
||||
}
|
||||
|
||||
// DeleteBucket will use the key passed to Delete as a bucket (database) name,
|
||||
//
|
||||
// and delete the bucket.
|
||||
//
|
||||
// This option should not be combined with the store.DeleteFrom option, as
|
||||
//
|
||||
// that will overwrite the delete action.
|
||||
func DeleteBucket() store.DeleteOption {
|
||||
return func(d *store.DeleteOptions) {
|
||||
d.Table = "DELETE_BUCKET"
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package natsjskv
|
||||
|
||||
import "go-micro.dev/v4/store"
|
||||
|
||||
type test struct {
|
||||
Record *store.Record
|
||||
Database string
|
||||
Table string
|
||||
}
|
||||
|
||||
var (
|
||||
table = []test{
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "One",
|
||||
Value: []byte("First value"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Two",
|
||||
Value: []byte("Second value"),
|
||||
},
|
||||
Table: "prefix_test",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Third",
|
||||
Value: []byte("Third value"),
|
||||
},
|
||||
Database: "new-bucket",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Four",
|
||||
Value: []byte("Fourth value"),
|
||||
},
|
||||
Database: "new-bucket",
|
||||
Table: "prefix_test",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "empty-value",
|
||||
Value: []byte{},
|
||||
},
|
||||
Database: "new-bucket",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Alex",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "names",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Jones",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "names",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Adrianna",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "names",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "MexicoCity",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "HoustonCity",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "ZurichCity",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Helsinki",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "testKeytest",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Table: "some_table",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "testSecondtest",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Table: "some_table",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "lalala",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Table: "some_table",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "testAnothertest",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "FobiddenCharactersAreAllowed:|@..+",
|
||||
Value: []byte("data no matter"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# NATS JetStream Store Plugin
|
||||
|
||||
> **WARNING**: NATS Object Storage is still marked as an experimental preview.
|
||||
|
||||
This plugin uses the NATS JetStream [Object Store](https://docs.nats.io/nats-concepts/jetstream/obj_store) to implement the Go-Micro store interface.
|
||||
|
||||
This enables a key/value store with unlimited message size, allowing you to store files or data of any size under a single key.
|
||||
|
||||
You can use this plugin like any other store plugin.
|
||||
To start a local NATS JetStream server run `nats-server -js`.
|
||||
|
||||
To manually create a new storage object call:
|
||||
|
||||
```go
|
||||
natsjs.NewStore(opts ...store.Option)
|
||||
```
|
||||
|
||||
The Go-Micro store interface uses databases and tables to store keys. These translate
|
||||
to buckets (object stores) and key prefixes. If no database (bucket name) is provided, "default" will be used.
|
||||
|
||||
You can call `Write` with any arbitrary database name, and if a bucket with that name does not exist yet,
|
||||
it will be automatically created.
|
||||
|
||||
If a table name is provided, it will use it to prefix the key as `<table>_<key>`.
|
||||
|
||||
To delete a bucket, and all the key/value pairs in it, pass the `DeleteBucket` option to the `Delete`
|
||||
method, then they key name will be interpreted as a bucket name, and the bucket will be deleted.
|
||||
|
||||
Next to the default store options, a few NATS specific options are available:
|
||||
|
||||
|
||||
```go
|
||||
// NatsOptions accepts nats.Options
|
||||
NatsOptions(opts nats.Options)
|
||||
|
||||
// JetStreamOptions accepts multiple nats.JSOpt
|
||||
JetStreamOptions(opts ...nats.JSOpt)
|
||||
|
||||
// ObjectStoreOptions accepts multiple nats.ObjectStoreConfigs
|
||||
// This will create buckets with the provided configs at initialization.
|
||||
//
|
||||
// type ObjectStoreConfig struct {
|
||||
// Bucket string
|
||||
// Description string
|
||||
// TTL time.Duration
|
||||
// MaxBytes int64
|
||||
// Storage StorageType (either memory or file storage)
|
||||
// Replicas int
|
||||
// Placement *Placement
|
||||
// }
|
||||
//
|
||||
ObjectStoreOptions(cfg ...*nats.ObjectStoreConfig)
|
||||
|
||||
// DefaultTTL sets the default TTL to use for new buckets
|
||||
// By default no TTL is set.
|
||||
//
|
||||
// TTL ON INDIVIDUAL WRITE CALLS IS NOT SUPPORTED, only bucket wide TTL.
|
||||
// Either set a default TTL with this option or provide bucket specific options
|
||||
// with ObjectStoreOptions
|
||||
DefaultTTL(ttl time.Duration)
|
||||
|
||||
// DefaultMemory sets the default storage type to memory only.
|
||||
//
|
||||
// The default is file storage, persisting storage between service restarts.
|
||||
// Be aware that the default storage location of NATS the /tmp dir is, and thus
|
||||
// won't persist reboots.
|
||||
DefaultMemory()
|
||||
|
||||
// DefaultDescription sets the default description to use when creating new
|
||||
// buckets. The default is "Store managed by go-micro"
|
||||
DefaultDescription(text string)
|
||||
|
||||
// DeleteBucket will use the key passed to Delete as a bucket (database) name,
|
||||
// and delete the bucket.
|
||||
// This option should not be combined with the store.DeleteFrom option, as
|
||||
// that will overwrite the delete action.
|
||||
DeleteBucket()
|
||||
```
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package natsjs
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// setStoreOption returns a function to setup a context with given value.
|
||||
func setStoreOption(k, v interface{}) store.Option {
|
||||
return func(o *store.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
package natsjs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cornelk/hashmap"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/pkg/errors"
|
||||
"go-micro.dev/v4/store"
|
||||
"go-micro.dev/v4/util/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBucketNotFound = errors.New("Bucket (database) not found")
|
||||
)
|
||||
|
||||
type natsStore struct {
|
||||
sync.Once
|
||||
sync.RWMutex
|
||||
|
||||
ttl time.Duration
|
||||
storageType nats.StorageType
|
||||
description string
|
||||
|
||||
opts store.Options
|
||||
nopts nats.Options
|
||||
jsopts []nats.JSOpt
|
||||
objStoreConfigs []*nats.ObjectStoreConfig
|
||||
|
||||
conn *nats.Conn
|
||||
js nats.JetStreamContext
|
||||
buckets *hashmap.Map[string, nats.ObjectStore]
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultStores["natsjs"] = NewStore
|
||||
}
|
||||
|
||||
// NewStore will create a new NATS JetStream Object Store.
|
||||
func NewStore(opts ...store.Option) store.Store {
|
||||
options := store.Options{
|
||||
Nodes: []string{},
|
||||
Database: "default",
|
||||
Table: "",
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
n := &natsStore{
|
||||
description: "Object storage administered by go-micro store plugin",
|
||||
opts: options,
|
||||
jsopts: []nats.JSOpt{},
|
||||
objStoreConfigs: []*nats.ObjectStoreConfig{},
|
||||
buckets: hashmap.New[string, nats.ObjectStore](),
|
||||
storageType: nats.FileStorage,
|
||||
}
|
||||
|
||||
n.setOption(opts...)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// Init initializes the store. It must perform any required setup on the
|
||||
// backing storage implementation and check that it is ready for use,
|
||||
// returning any errors.
|
||||
func (n *natsStore) Init(opts ...store.Option) error {
|
||||
n.setOption(opts...)
|
||||
|
||||
// Connect to NATS servers
|
||||
conn, err := n.nopts.Connect()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to connect to NATS Server")
|
||||
}
|
||||
n.conn = conn
|
||||
|
||||
// Create JetStream context
|
||||
js, err := conn.JetStream(n.jsopts...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create JetStream context")
|
||||
}
|
||||
n.js = js
|
||||
|
||||
// Create default config if no configs present
|
||||
if len(n.objStoreConfigs) == 0 {
|
||||
n.objStoreConfigs = append(n.objStoreConfigs, &nats.ObjectStoreConfig{
|
||||
Bucket: n.opts.Database,
|
||||
Description: n.description,
|
||||
TTL: n.ttl,
|
||||
Storage: n.storageType,
|
||||
})
|
||||
}
|
||||
|
||||
// Create objest store buckets
|
||||
for _, cfg := range n.objStoreConfigs {
|
||||
store, err := js.CreateObjectStore(cfg)
|
||||
if err == nats.ErrStreamNameAlreadyInUse {
|
||||
store, err = n.js.ObjectStore(cfg.Bucket)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to create bucket (%s)", cfg.Bucket)
|
||||
}
|
||||
n.buckets.Set(cfg.Bucket, store)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *natsStore) setOption(opts ...store.Option) {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
}
|
||||
|
||||
n.Once.Do(func() {
|
||||
n.nopts = nats.GetDefaultOptions()
|
||||
})
|
||||
|
||||
// Extract options from context
|
||||
if nopts, ok := n.opts.Context.Value(natsOptionsKey{}).(nats.Options); ok {
|
||||
n.nopts = nopts
|
||||
}
|
||||
|
||||
if jsopts, ok := n.opts.Context.Value(jsOptionsKey{}).([]nats.JSOpt); ok {
|
||||
n.jsopts = append(n.jsopts, jsopts...)
|
||||
}
|
||||
|
||||
if cfg, ok := n.opts.Context.Value(objOptionsKey{}).([]*nats.ObjectStoreConfig); ok {
|
||||
n.objStoreConfigs = append(n.objStoreConfigs, cfg...)
|
||||
}
|
||||
|
||||
if ttl, ok := n.opts.Context.Value(ttlOptionsKey{}).(time.Duration); ok {
|
||||
n.ttl = ttl
|
||||
}
|
||||
|
||||
if sType, ok := n.opts.Context.Value(memoryOptionsKey{}).(nats.StorageType); ok {
|
||||
n.storageType = sType
|
||||
}
|
||||
|
||||
if text, ok := n.opts.Context.Value(descriptionOptionsKey{}).(string); ok {
|
||||
n.description = text
|
||||
}
|
||||
|
||||
// Assign store option server addresses to nats options
|
||||
if len(n.opts.Nodes) > 0 {
|
||||
n.nopts.Url = ""
|
||||
n.nopts.Servers = n.opts.Nodes
|
||||
}
|
||||
|
||||
if len(n.nopts.Servers) == 0 && n.nopts.Url == "" {
|
||||
n.nopts.Url = nats.DefaultURL
|
||||
}
|
||||
}
|
||||
|
||||
// Options allows you to view the current options.
|
||||
func (n *natsStore) Options() store.Options {
|
||||
return n.opts
|
||||
}
|
||||
|
||||
// Read takes a single key name and optional ReadOptions. It returns matching []*Record or an error.
|
||||
func (n *natsStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
if err := n.initConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := store.ReadOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
bucket, ok := n.buckets.Get(opt.Database)
|
||||
if !ok {
|
||||
return nil, ErrBucketNotFound
|
||||
}
|
||||
|
||||
var keys []string
|
||||
|
||||
var keyPrefix, keySuffix string
|
||||
|
||||
switch {
|
||||
case opt.Prefix:
|
||||
keyPrefix = getKey(key, opt.Table)
|
||||
case opt.Suffix:
|
||||
keySuffix = key
|
||||
default:
|
||||
keys = []string{getKey(key, opt.Table)}
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
objects, err := bucket.List()
|
||||
if err == nats.ErrNoObjectsFound {
|
||||
return []*store.Record{}, nil
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to list objects")
|
||||
}
|
||||
|
||||
for _, obj := range objects {
|
||||
name := obj.Name
|
||||
if !strings.HasPrefix(name, opt.Table) {
|
||||
continue
|
||||
}
|
||||
if (!opt.Prefix && !opt.Suffix) && key != name {
|
||||
continue
|
||||
}
|
||||
if opt.Prefix && !strings.HasPrefix(name, keyPrefix) {
|
||||
continue
|
||||
}
|
||||
if opt.Suffix && !strings.HasSuffix(name, keySuffix) {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, name)
|
||||
}
|
||||
}
|
||||
|
||||
records := []*store.Record{}
|
||||
for _, key := range keys {
|
||||
obj, err := bucket.Get(key)
|
||||
if err == nats.ErrObjectNotFound {
|
||||
return []*store.Record{}, nil
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get object from bucket")
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
info, err := obj.Info()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to fetch record info")
|
||||
}
|
||||
|
||||
if info.Deleted {
|
||||
continue
|
||||
}
|
||||
|
||||
metadata := map[string]interface{}{}
|
||||
for key, value := range info.Headers {
|
||||
var val interface{}
|
||||
if err := json.Unmarshal([]byte(value[0]), &val); err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to JSON unmarshal metadata")
|
||||
}
|
||||
metadata[key] = val
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(obj)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to read returned bytes")
|
||||
}
|
||||
|
||||
records = append(records, &store.Record{
|
||||
Key: key,
|
||||
Value: b,
|
||||
Metadata: metadata,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if opt.Limit > 0 {
|
||||
return records[opt.Offset : opt.Offset+opt.Limit], nil
|
||||
}
|
||||
if opt.Offset > 0 {
|
||||
return records[opt.Offset:], nil
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// Write writes a record to the store, and returns an error if the record was not written.
|
||||
func (n *natsStore) Write(r *store.Record, opts ...store.WriteOption) error {
|
||||
if err := n.initConn(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := store.WriteOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
store, ok := n.buckets.Get(opt.Database)
|
||||
// Create new bucket if not exists
|
||||
if !ok {
|
||||
var err error
|
||||
store, err = n.createNewBucket(opt.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
header := nats.Header{}
|
||||
for key, value := range r.Metadata {
|
||||
val, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to JSON marshal metadata")
|
||||
}
|
||||
header.Set(key, string(val))
|
||||
}
|
||||
|
||||
_, err := store.Put(&nats.ObjectMeta{
|
||||
Name: getKey(r.Key, opt.Table),
|
||||
Description: "Store managed by go-micro",
|
||||
Headers: header,
|
||||
}, bytes.NewReader(r.Value))
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to store data in bucket")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes the record with the corresponding key from the store.
|
||||
func (n *natsStore) Delete(key string, opts ...store.DeleteOption) error {
|
||||
if err := n.initConn(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := store.DeleteOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
if opt.Table == "DELETE_BUCKET" {
|
||||
n.buckets.Del(key)
|
||||
if err := n.js.DeleteObjectStore(key); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete bucket")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
store, ok := n.buckets.Get(opt.Database)
|
||||
if !ok {
|
||||
return ErrBucketNotFound
|
||||
}
|
||||
|
||||
if err := store.Delete(getKey(key, opt.Table)); err != nil {
|
||||
return errors.Wrap(err, "Failed to delete data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns any keys that match, or an empty list with no error if none matched.
|
||||
func (n *natsStore) List(opts ...store.ListOption) ([]string, error) {
|
||||
if err := n.initConn(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := store.ListOptions{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
if opt.Database == "" {
|
||||
opt.Database = n.opts.Database
|
||||
}
|
||||
if opt.Table == "" {
|
||||
opt.Table = n.opts.Table
|
||||
}
|
||||
|
||||
store, ok := n.buckets.Get(opt.Database)
|
||||
if !ok {
|
||||
return nil, ErrBucketNotFound
|
||||
}
|
||||
|
||||
objects, err := store.List()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to list keys in bucket")
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for _, obj := range objects {
|
||||
key := obj.Name
|
||||
|
||||
if !strings.HasPrefix(key, getKey(opt.Prefix, opt.Table)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(key, opt.Suffix) {
|
||||
continue
|
||||
}
|
||||
|
||||
keys = append(keys, strings.TrimPrefix(key, getKey(opt.Prefix, opt.Table)))
|
||||
}
|
||||
|
||||
if opt.Limit > 0 && int(opt.Offset+opt.Limit) < len(keys) {
|
||||
return keys[opt.Offset : opt.Offset+opt.Limit], nil
|
||||
}
|
||||
if opt.Offset > 0 && int(opt.Offset) < len(keys) {
|
||||
return keys[opt.Offset:], nil
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// Close the store.
|
||||
func (n *natsStore) Close() error {
|
||||
n.conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the name of the implementation.
|
||||
func (n *natsStore) String() string {
|
||||
return "NATS JetStream ObjectStore"
|
||||
}
|
||||
|
||||
func getKey(key, table string) string {
|
||||
if table != "" {
|
||||
key = table + "_" + key
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (n *natsStore) createNewBucket(name string) (nats.ObjectStore, error) {
|
||||
store, err := n.js.CreateObjectStore(&nats.ObjectStoreConfig{
|
||||
Bucket: name,
|
||||
Description: n.description,
|
||||
TTL: n.ttl,
|
||||
Storage: n.storageType,
|
||||
})
|
||||
if err == nats.ErrStreamNameAlreadyInUse {
|
||||
store, err = n.js.ObjectStore(name)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to create new bucket (%s)", name)
|
||||
}
|
||||
n.buckets.Set(name, store)
|
||||
return store, err
|
||||
}
|
||||
|
||||
// thread safe way to initialize the connection.
|
||||
func (n *natsStore) initConn() error {
|
||||
if n.hasConn() {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
// check if conn was initialized meanwhile
|
||||
if n.conn != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return n.Init()
|
||||
}
|
||||
|
||||
// thread safe way to check if n is initialized.
|
||||
func (n *natsStore) hasConn() bool {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
return n.conn != nil
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package natsjs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
// store.Option.
|
||||
type natsOptionsKey struct{}
|
||||
type jsOptionsKey struct{}
|
||||
type objOptionsKey struct{}
|
||||
type ttlOptionsKey struct{}
|
||||
type memoryOptionsKey struct{}
|
||||
type descriptionOptionsKey struct{}
|
||||
|
||||
// store.DeleteOption.
|
||||
type delBucketOptionsKey struct{}
|
||||
|
||||
// NatsOptions accepts nats.Options.
|
||||
func NatsOptions(opts nats.Options) store.Option {
|
||||
return setStoreOption(natsOptionsKey{}, opts)
|
||||
}
|
||||
|
||||
// JetStreamOptions accepts multiple nats.JSOpt.
|
||||
func JetStreamOptions(opts ...nats.JSOpt) store.Option {
|
||||
return setStoreOption(jsOptionsKey{}, opts)
|
||||
}
|
||||
|
||||
// ObjectStoreOptions accepts multiple nats.ObjectStoreConfigs
|
||||
// This will create buckets with the provided configs at initialization.
|
||||
func ObjectStoreOptions(cfg ...*nats.ObjectStoreConfig) store.Option {
|
||||
return setStoreOption(objOptionsKey{}, cfg)
|
||||
}
|
||||
|
||||
// DefaultTTL sets the default TTL to use for new buckets
|
||||
//
|
||||
// By default no TTL is set.
|
||||
//
|
||||
// TTL ON INDIVIDUAL WRITE CALLS IS NOT SUPPORTED, only bucket wide TTL.
|
||||
// Either set a default TTL with this option or provide bucket specific options
|
||||
//
|
||||
// with ObjectStoreOptions
|
||||
func DefaultTTL(ttl time.Duration) store.Option {
|
||||
return setStoreOption(ttlOptionsKey{}, ttl)
|
||||
}
|
||||
|
||||
// DefaultMemory sets the default storage type to memory only.
|
||||
//
|
||||
// The default is file storage, persisting storage between service restarts.
|
||||
//
|
||||
// Be aware that the default storage location of NATS the /tmp dir is, and thus
|
||||
//
|
||||
// won't persist reboots.
|
||||
func DefaultMemory() store.Option {
|
||||
return setStoreOption(memoryOptionsKey{}, nats.MemoryStorage)
|
||||
}
|
||||
|
||||
// DefaultDescription sets the default description to use when creating new
|
||||
//
|
||||
// buckets. The default is "Store managed by go-micro"
|
||||
func DefaultDescription(text string) store.Option {
|
||||
return setStoreOption(descriptionOptionsKey{}, text)
|
||||
}
|
||||
|
||||
// DeleteBucket will use the key passed to Delete as a bucket (database) name,
|
||||
//
|
||||
// and delete the bucket.
|
||||
//
|
||||
// This option should not be combined with the store.DeleteFrom option, as
|
||||
//
|
||||
// that will overwrite the delete action.
|
||||
func DeleteBucket() store.DeleteOption {
|
||||
return func(d *store.DeleteOptions) {
|
||||
d.Table = "DELETE_BUCKET"
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package natsjs
|
||||
|
||||
import "go-micro.dev/v4/store"
|
||||
|
||||
type test struct {
|
||||
Record *store.Record
|
||||
Database string
|
||||
Table string
|
||||
}
|
||||
|
||||
var (
|
||||
table = []test{
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "One",
|
||||
Value: []byte("First value"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Two",
|
||||
Value: []byte("Second value"),
|
||||
},
|
||||
Table: "prefix_test",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Third",
|
||||
Value: []byte("Third value"),
|
||||
},
|
||||
Database: "new-bucket",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Four",
|
||||
Value: []byte("Fourth value"),
|
||||
},
|
||||
Database: "new-bucket",
|
||||
Table: "prefix_test",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "empty-value",
|
||||
Value: []byte{},
|
||||
},
|
||||
Database: "new-bucket",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Alex",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "names",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Jones",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "names",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Adrianna",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "names",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "MexicoCity",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "HoustonCity",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "ZurichCity",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "Helsinki",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Database: "prefix-test",
|
||||
Table: "cities",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "testKeytest",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Table: "some_table",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "testSecondtest",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Table: "some_table",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "lalala",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
Table: "some_table",
|
||||
},
|
||||
{
|
||||
Record: &store.Record{
|
||||
Key: "testAnothertest",
|
||||
Value: []byte("Some value"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"go-micro.dev/v4/store"
|
||||
)
|
||||
|
||||
type redisOptionsContextKey struct{}
|
||||
|
||||
// WithRedisOptions sets advanced options for redis.
|
||||
func WithRedisOptions(options redis.UniversalOptions) store.Option {
|
||||
return func(o *store.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
|
||||
o.Context = context.WithValue(o.Context, redisOptionsContextKey{}, options)
|
||||
}
|
||||
}
|
||||
|
||||
func newUniversalClient(o store.Options) redis.UniversalClient {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
|
||||
opts, ok := o.Context.Value(redisOptionsContextKey{}).(redis.UniversalOptions)
|
||||
if !ok && len(o.Nodes) <= 1 {
|
||||
addr := "redis://127.0.0.1:6379"
|
||||
if len(o.Nodes) > 0 {
|
||||
addr = o.Nodes[0]
|
||||
}
|
||||
|
||||
redisOptions, err := redis.ParseURL(addr)
|
||||
if err != nil {
|
||||
redisOptions = &redis.Options{Addr: addr}
|
||||
}
|
||||
|
||||
return redis.NewClient(redisOptions)
|
||||
}
|
||||
|
||||
if len(opts.Addrs) == 0 && len(o.Nodes) > 0 {
|
||||
opts.Addrs = o.Nodes
|
||||
}
|
||||
|
||||
return redis.NewUniversalClient(&opts)
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// Package redis is a redis backed store implementation
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/store"
|
||||
"go-micro.dev/v4/util/cmd"
|
||||
)
|
||||
|
||||
// DefaultDatabase is the namespace that the store
|
||||
// will use if no namespace is provided.
|
||||
var (
|
||||
DefaultDatabase = "micro"
|
||||
DefaultTable = "micro"
|
||||
)
|
||||
|
||||
type rkv struct {
|
||||
ctx context.Context
|
||||
options store.Options
|
||||
Client redis.UniversalClient
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmd.DefaultStores["redis"] = NewStore
|
||||
}
|
||||
|
||||
func (r *rkv) Init(opts ...store.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&r.options)
|
||||
}
|
||||
|
||||
return r.configure()
|
||||
}
|
||||
|
||||
func (r *rkv) Close() error {
|
||||
return r.Client.Close()
|
||||
}
|
||||
|
||||
func (r *rkv) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
options := store.ReadOptions{
|
||||
Table: r.options.Table,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
var keys []string
|
||||
|
||||
var rkey string
|
||||
|
||||
switch {
|
||||
case options.Prefix:
|
||||
rkey = fmt.Sprintf("%s%s*", options.Table, key)
|
||||
case options.Suffix:
|
||||
rkey = fmt.Sprintf("%s*%s", options.Table, key)
|
||||
default:
|
||||
keys = []string{fmt.Sprintf("%s%s", options.Table, key)}
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
cursor := uint64(options.Offset)
|
||||
count := int64(options.Limit)
|
||||
|
||||
for {
|
||||
var err error
|
||||
|
||||
var ks []string
|
||||
|
||||
ks, cursor, err = r.Client.Scan(r.ctx, cursor, rkey, count).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys = append(keys, ks...)
|
||||
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
records := make([]*store.Record, 0, len(keys))
|
||||
|
||||
// read all keys, continue on error
|
||||
var val []byte
|
||||
|
||||
var d time.Duration
|
||||
|
||||
var err error
|
||||
|
||||
for _, rkey = range keys {
|
||||
val, err = r.Client.Get(r.ctx, rkey).Bytes()
|
||||
if err != nil || val == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
d, err = r.Client.TTL(r.ctx, rkey).Result()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
records = append(records, &store.Record{
|
||||
Key: key,
|
||||
Value: val,
|
||||
Expiry: d,
|
||||
})
|
||||
}
|
||||
|
||||
if len(keys) == 1 {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return records, store.ErrNotFound
|
||||
}
|
||||
return records, err
|
||||
}
|
||||
|
||||
// keys might have vanished since we scanned them, ignore errors
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *rkv) Delete(key string, opts ...store.DeleteOption) error {
|
||||
options := store.DeleteOptions{
|
||||
Table: r.options.Table,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
rkey := fmt.Sprintf("%s%s", options.Table, key)
|
||||
|
||||
return r.Client.Del(r.ctx, rkey).Err()
|
||||
}
|
||||
|
||||
func (r *rkv) Write(record *store.Record, opts ...store.WriteOption) error {
|
||||
options := store.WriteOptions{
|
||||
Table: r.options.Table,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
rkey := fmt.Sprintf("%s%s", options.Table, record.Key)
|
||||
|
||||
return r.Client.Set(r.ctx, rkey, record.Value, record.Expiry).Err()
|
||||
}
|
||||
|
||||
func (r *rkv) List(opts ...store.ListOption) ([]string, error) {
|
||||
options := store.ListOptions{
|
||||
Table: r.options.Table,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s%s*%s", options.Table, options.Prefix, options.Suffix)
|
||||
|
||||
cursor := uint64(options.Offset)
|
||||
|
||||
count := int64(options.Limit)
|
||||
|
||||
var allKeys []string
|
||||
|
||||
var keys []string
|
||||
|
||||
var err error
|
||||
|
||||
for {
|
||||
keys, cursor, err = r.Client.Scan(r.ctx, cursor, key, count).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, key := range keys {
|
||||
keys[i] = strings.TrimPrefix(key, options.Table)
|
||||
}
|
||||
|
||||
allKeys = append(allKeys, keys...)
|
||||
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return allKeys, nil
|
||||
}
|
||||
|
||||
func (r *rkv) Options() store.Options {
|
||||
return r.options
|
||||
}
|
||||
|
||||
func (r *rkv) String() string {
|
||||
return "redis"
|
||||
}
|
||||
|
||||
// NewStore returns a redis store.
|
||||
func NewStore(opts ...store.Option) store.Store {
|
||||
options := store.Options{
|
||||
Database: DefaultDatabase,
|
||||
Table: DefaultTable,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
s := &rkv{
|
||||
ctx: context.Background(),
|
||||
options: options,
|
||||
}
|
||||
|
||||
if err := s.configure(); err != nil {
|
||||
s.options.Logger.Log(logger.ErrorLevel, "Error configuring store ", err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *rkv) configure() error {
|
||||
if r.Client != nil {
|
||||
if err := r.Client.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.Client = newUniversalClient(r.options)
|
||||
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
# Prometheus
|
||||
|
||||
Wrappers are a form of middleware that can be used with go-micro services. They can wrap both the Client and Server handlers.
|
||||
This plugin implements the HandlerWrapper interface to provide automatic prometheus metric handling
|
||||
for each microservice method execution time and operation count for success and failed cases.
|
||||
|
||||
This handler will export two metrics to prometheus:
|
||||
* **micro_request_total**. How many go-micro requests processed, partitioned by method and status.
|
||||
* **micro_request_duration_microseconds**. Service method request latencies in microseconds, partitioned by method.
|
||||
|
||||
# Usage
|
||||
|
||||
When creating your service, add the wrapper like so.
|
||||
|
||||
```go
|
||||
service := micro.NewService(
|
||||
micro.Name("service name"),
|
||||
micro.Version("latest"),
|
||||
micro.WrapHandler(prometheus.NewHandlerWrapper()),
|
||||
)
|
||||
|
||||
service.Init()
|
||||
```
|
||||
|
||||
Generated
Vendored
+294
@@ -0,0 +1,294 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/logger"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
)
|
||||
|
||||
var (
|
||||
// default metric prefix.
|
||||
DefaultMetricPrefix = "micro_"
|
||||
// default label prefix.
|
||||
DefaultLabelPrefix = "micro_"
|
||||
|
||||
opsCounter *prometheus.CounterVec
|
||||
timeCounterSummary *prometheus.SummaryVec
|
||||
timeCounterHistogram *prometheus.HistogramVec
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Name string
|
||||
Version string
|
||||
ID string
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func ServiceName(name string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func ServiceVersion(version string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
func ServiceID(id string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
if opsCounter == nil {
|
||||
opsCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: fmt.Sprintf("%srequest_total", DefaultMetricPrefix),
|
||||
Help: "Requests processed, partitioned by endpoint and status",
|
||||
},
|
||||
[]string{
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "name"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "version"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "id"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "endpoint"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "status"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if timeCounterSummary == nil {
|
||||
timeCounterSummary = prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: fmt.Sprintf("%slatency_microseconds", DefaultMetricPrefix),
|
||||
Help: "Request latencies in microseconds, partitioned by endpoint",
|
||||
},
|
||||
[]string{
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "name"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "version"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "id"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "endpoint"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if timeCounterHistogram == nil {
|
||||
timeCounterHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: fmt.Sprintf("%srequest_duration_seconds", DefaultMetricPrefix),
|
||||
Help: "Request time in seconds, partitioned by endpoint",
|
||||
},
|
||||
[]string{
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "name"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "version"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "id"),
|
||||
fmt.Sprintf("%s%s", DefaultLabelPrefix, "endpoint"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for _, collector := range []prometheus.Collector{opsCounter, timeCounterSummary, timeCounterHistogram} {
|
||||
if err := prometheus.DefaultRegisterer.Register(collector); err != nil {
|
||||
// if already registered, skip fatal
|
||||
if _, ok := err.(prometheus.AlreadyRegisteredError); !ok {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
options Options
|
||||
callFunc client.CallFunc
|
||||
client.Client
|
||||
}
|
||||
|
||||
func NewClientWrapper(opts ...Option) client.Wrapper {
|
||||
options := Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return func(c client.Client) client.Client {
|
||||
handler := &wrapper{
|
||||
options: options,
|
||||
Client: c,
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
}
|
||||
|
||||
func NewCallWrapper(opts ...Option) client.CallWrapper {
|
||||
options := Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return func(fn client.CallFunc) client.CallFunc {
|
||||
handler := &wrapper{
|
||||
options: options,
|
||||
callFunc: fn,
|
||||
}
|
||||
|
||||
return handler.CallFunc
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wrapper) CallFunc(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000 // make microseconds
|
||||
timeCounterSummary.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(us)
|
||||
timeCounterHistogram.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(v)
|
||||
}))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
err := w.callFunc(ctx, node, req, rsp, opts)
|
||||
if err == nil {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "success").Inc()
|
||||
} else {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "failure").Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *wrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000 // make microseconds
|
||||
timeCounterSummary.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(us)
|
||||
timeCounterHistogram.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(v)
|
||||
}))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
err := w.Client.Call(ctx, req, rsp, opts...)
|
||||
if err == nil {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "success").Inc()
|
||||
} else {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "failure").Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *wrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
||||
endpoint := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000 // make microseconds
|
||||
timeCounterSummary.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(us)
|
||||
timeCounterHistogram.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(v)
|
||||
}))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
stream, err := w.Client.Stream(ctx, req, opts...)
|
||||
if err == nil {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "success").Inc()
|
||||
} else {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "failure").Inc()
|
||||
}
|
||||
|
||||
return stream, err
|
||||
}
|
||||
|
||||
func (w *wrapper) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
|
||||
endpoint := p.Topic()
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000 // make microseconds
|
||||
timeCounterSummary.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(us)
|
||||
timeCounterHistogram.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(v)
|
||||
}))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
err := w.Client.Publish(ctx, p, opts...)
|
||||
if err == nil {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "success").Inc()
|
||||
} else {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "failure").Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewHandlerWrapper(opts ...Option) server.HandlerWrapper {
|
||||
options := Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
handler := &wrapper{
|
||||
options: options,
|
||||
}
|
||||
|
||||
return handler.HandlerFunc
|
||||
}
|
||||
|
||||
func (w *wrapper) HandlerFunc(fn server.HandlerFunc) server.HandlerFunc {
|
||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||
endpoint := req.Endpoint()
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000 // make microseconds
|
||||
timeCounterSummary.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(us)
|
||||
timeCounterHistogram.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(v)
|
||||
}))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
err := fn(ctx, req, rsp)
|
||||
if err == nil {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "success").Inc()
|
||||
} else {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "failure").Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func NewSubscriberWrapper(opts ...Option) server.SubscriberWrapper {
|
||||
options := Options{}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
handler := &wrapper{
|
||||
options: options,
|
||||
}
|
||||
|
||||
return handler.SubscriberFunc
|
||||
}
|
||||
|
||||
func (w *wrapper) SubscriberFunc(fn server.SubscriberFunc) server.SubscriberFunc {
|
||||
return func(ctx context.Context, msg server.Message) error {
|
||||
endpoint := msg.Topic()
|
||||
|
||||
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
|
||||
us := v * 1000000 // make microseconds
|
||||
timeCounterSummary.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(us)
|
||||
timeCounterHistogram.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint).Observe(v)
|
||||
}))
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
err := fn(ctx, msg)
|
||||
if err == nil {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "success").Inc()
|
||||
} else {
|
||||
opsCounter.WithLabelValues(w.options.Name, w.options.Version, w.options.ID, endpoint, "failure").Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015 Asim Aslam.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
# OpenTelemetry wrappers
|
||||
|
||||
OpenTelemetry wrappers propagate traces (spans) accross services.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
service := micro.NewService(
|
||||
micro.Name("go.micro.srv.greeter"),
|
||||
micro.WrapClient(opentelemetry.NewClientWrapper()),
|
||||
micro.WrapHandler(open.NewHandlerWrapper()),
|
||||
micro.WrapSubscriber(opentelemetry.NewSubscriberWrapper()),
|
||||
)
|
||||
```
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go-micro.dev/v4/metadata"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/baggage"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
instrumentationName = "github.com/go-micro/plugins/v4/wrapper/trace/opentelemetry"
|
||||
)
|
||||
|
||||
// StartSpanFromContext returns a new span with the given operation name and options. If a span
|
||||
// is found in the context, it will be used as the parent of the resulting span.
|
||||
func StartSpanFromContext(ctx context.Context, tp trace.TracerProvider, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
md, ok := metadata.FromContext(ctx)
|
||||
if !ok {
|
||||
md = make(metadata.Metadata)
|
||||
}
|
||||
propagator, carrier := otel.GetTextMapPropagator(), make(propagation.MapCarrier)
|
||||
for k, v := range md {
|
||||
for _, f := range propagator.Fields() {
|
||||
if strings.EqualFold(k, f) {
|
||||
carrier[f] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx = propagator.Extract(ctx, carrier)
|
||||
spanCtx := trace.SpanContextFromContext(ctx)
|
||||
ctx = baggage.ContextWithBaggage(ctx, baggage.FromContext(ctx))
|
||||
|
||||
var tracer trace.Tracer
|
||||
var span trace.Span
|
||||
if tp != nil {
|
||||
tracer = tp.Tracer(instrumentationName)
|
||||
} else {
|
||||
tracer = otel.Tracer(instrumentationName)
|
||||
}
|
||||
ctx, span = tracer.Start(trace.ContextWithRemoteSpanContext(ctx, spanCtx), name, opts...)
|
||||
|
||||
carrier = make(propagation.MapCarrier)
|
||||
propagator.Inject(ctx, carrier)
|
||||
for k, v := range carrier {
|
||||
//lint:ignore SA1019 no unicode punctution handle needed
|
||||
md.Set(strings.Title(k), v)
|
||||
}
|
||||
ctx = metadata.NewContext(ctx, md)
|
||||
|
||||
return ctx, span
|
||||
}
|
||||
Generated
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/server"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
TraceProvider trace.TracerProvider
|
||||
|
||||
CallFilter CallFilter
|
||||
StreamFilter StreamFilter
|
||||
PublishFilter PublishFilter
|
||||
SubscriberFilter SubscriberFilter
|
||||
HandlerFilter HandlerFilter
|
||||
}
|
||||
|
||||
// CallFilter used to filter client.Call, return true to skip call trace.
|
||||
type CallFilter func(context.Context, client.Request) bool
|
||||
|
||||
// StreamFilter used to filter client.Stream, return true to skip stream trace.
|
||||
type StreamFilter func(context.Context, client.Request) bool
|
||||
|
||||
// PublishFilter used to filter client.Publish, return true to skip publish trace.
|
||||
type PublishFilter func(context.Context, client.Message) bool
|
||||
|
||||
// SubscriberFilter used to filter server.Subscribe, return true to skip subcribe trace.
|
||||
type SubscriberFilter func(context.Context, server.Message) bool
|
||||
|
||||
// HandlerFilter used to filter server.Handle, return true to skip handle trace.
|
||||
type HandlerFilter func(context.Context, server.Request) bool
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func WithTraceProvider(tp trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TraceProvider = tp
|
||||
}
|
||||
}
|
||||
|
||||
func WithCallFilter(filter CallFilter) Option {
|
||||
return func(o *Options) {
|
||||
o.CallFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
func WithStreamFilter(filter StreamFilter) Option {
|
||||
return func(o *Options) {
|
||||
o.StreamFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
func WithPublishFilter(filter PublishFilter) Option {
|
||||
return func(o *Options) {
|
||||
o.PublishFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
func WithSubscribeFilter(filter SubscriberFilter) Option {
|
||||
return func(o *Options) {
|
||||
o.SubscriberFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
func WithHandleFilter(filter HandlerFilter) Option {
|
||||
return func(o *Options) {
|
||||
o.HandlerFilter = filter
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+175
@@ -0,0 +1,175 @@
|
||||
package opentelemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go-micro.dev/v4/client"
|
||||
"go-micro.dev/v4/registry"
|
||||
"go-micro.dev/v4/server"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// NewCallWrapper accepts an opentracing Tracer and returns a Call Wrapper.
|
||||
func NewCallWrapper(opts ...Option) client.CallWrapper {
|
||||
options := Options{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return func(cf client.CallFunc) client.CallFunc {
|
||||
return func(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||
if options.CallFilter != nil && options.CallFilter(ctx, req) {
|
||||
return cf(ctx, node, req, rsp, opts)
|
||||
}
|
||||
name := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
}
|
||||
ctx, span := StartSpanFromContext(ctx, options.TraceProvider, name, spanOpts...)
|
||||
defer span.End()
|
||||
if err := cf(ctx, node, req, rsp, opts); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewHandlerWrapper accepts an opentracing Tracer and returns a Handler Wrapper.
|
||||
func NewHandlerWrapper(opts ...Option) server.HandlerWrapper {
|
||||
options := Options{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return func(h server.HandlerFunc) server.HandlerFunc {
|
||||
return func(ctx context.Context, req server.Request, rsp interface{}) error {
|
||||
if options.HandlerFilter != nil && options.HandlerFilter(ctx, req) {
|
||||
return h(ctx, req, rsp)
|
||||
}
|
||||
name := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindServer),
|
||||
}
|
||||
ctx, span := StartSpanFromContext(ctx, options.TraceProvider, name, spanOpts...)
|
||||
defer span.End()
|
||||
if err := h(ctx, req, rsp); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewSubscriberWrapper accepts an opentracing Tracer and returns a Subscriber Wrapper.
|
||||
func NewSubscriberWrapper(opts ...Option) server.SubscriberWrapper {
|
||||
options := Options{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return func(next server.SubscriberFunc) server.SubscriberFunc {
|
||||
return func(ctx context.Context, msg server.Message) error {
|
||||
if options.SubscriberFilter != nil && options.SubscriberFilter(ctx, msg) {
|
||||
return next(ctx, msg)
|
||||
}
|
||||
name := "Sub from " + msg.Topic()
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindServer),
|
||||
}
|
||||
ctx, span := StartSpanFromContext(ctx, options.TraceProvider, name, spanOpts...)
|
||||
defer span.End()
|
||||
if err := next(ctx, msg); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWrapper returns a client.Wrapper
|
||||
// that adds monitoring to outgoing requests.
|
||||
func NewClientWrapper(opts ...Option) client.Wrapper {
|
||||
options := Options{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
return func(c client.Client) client.Client {
|
||||
w := &clientWrapper{
|
||||
Client: c,
|
||||
tp: options.TraceProvider,
|
||||
callFilter: options.CallFilter,
|
||||
streamFilter: options.StreamFilter,
|
||||
publishFilter: options.PublishFilter,
|
||||
}
|
||||
return w
|
||||
}
|
||||
}
|
||||
|
||||
type clientWrapper struct {
|
||||
client.Client
|
||||
|
||||
tp trace.TracerProvider
|
||||
callFilter CallFilter
|
||||
streamFilter StreamFilter
|
||||
publishFilter PublishFilter
|
||||
}
|
||||
|
||||
func (w *clientWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
|
||||
if w.callFilter != nil && w.callFilter(ctx, req) {
|
||||
return w.Client.Call(ctx, req, rsp, opts...)
|
||||
}
|
||||
name := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
}
|
||||
ctx, span := StartSpanFromContext(ctx, w.tp, name, spanOpts...)
|
||||
defer span.End()
|
||||
if err := w.Client.Call(ctx, req, rsp, opts...); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *clientWrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
|
||||
if w.streamFilter != nil && w.streamFilter(ctx, req) {
|
||||
return w.Client.Stream(ctx, req, opts...)
|
||||
}
|
||||
name := fmt.Sprintf("%s.%s", req.Service(), req.Endpoint())
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
}
|
||||
ctx, span := StartSpanFromContext(ctx, w.tp, name, spanOpts...)
|
||||
defer span.End()
|
||||
stream, err := w.Client.Stream(ctx, req, opts...)
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
}
|
||||
return stream, err
|
||||
}
|
||||
|
||||
func (w *clientWrapper) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
|
||||
if w.publishFilter != nil && w.publishFilter(ctx, p) {
|
||||
return w.Client.Publish(ctx, p, opts...)
|
||||
}
|
||||
name := fmt.Sprintf("Pub to %s", p.Topic())
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
}
|
||||
ctx, span := StartSpanFromContext(ctx, w.tp, name, spanOpts...)
|
||||
defer span.End()
|
||||
if err := w.Client.Publish(ctx, p, opts...); err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user