[ADDED] Service api improvements (#1160)

Co-authored-by: Tomasz Pietrek <melgaer@gmail.com>
This commit is contained in:
Piotr Piotrowski
2022-12-20 12:52:16 +01:00
committed by GitHub
parent 95a7e5090f
commit 32b9daa3d2
15 changed files with 2151 additions and 698 deletions

4
js.go
View File

@@ -1528,13 +1528,11 @@ func (js *js) subscribe(subj, queue string, cb MsgHandler, ch chan *Msg, isSync,
} }
// Find the stream mapped to the subject if not bound to a stream already. // Find the stream mapped to the subject if not bound to a stream already.
if o.stream == _EMPTY_ { if stream == _EMPTY_ {
stream, err = js.StreamNameBySubject(subj) stream, err = js.StreamNameBySubject(subj)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
stream = o.stream
} }
// With an explicit durable name, we can lookup the consumer first // With an explicit durable name, we can lookup the consumer first

View File

@@ -0,0 +1,84 @@
// Copyright 2022 The NATS Authors
// 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.
package micro
import (
"fmt"
"log"
"strconv"
"time"
"github.com/nats-io/nats.go"
)
func Example() {
s := RunServerOnPort(-1)
defer s.Shutdown()
nc, err := nats.Connect(s.ClientURL())
if err != nil {
log.Fatal(err)
}
defer nc.Close()
// Service handler is a function which takes Service.Request as argument.
// req.Respond or req.Error should be used to respond to the request.
incrementHandler := func(req *Request) error {
val, err := strconv.Atoi(string(req.Data))
if err != nil {
req.Error("400", "request data should be a number", nil)
return nil
}
responseData := val + 1
req.Respond([]byte(strconv.Itoa(responseData)))
return nil
}
config := Config{
Name: "IncrementService",
Version: "0.1.0",
Description: "Increment numbers",
Endpoint: Endpoint{
// service handler
Handler: incrementHandler,
// a unique subject serving as a service endpoint
Subject: "numbers.increment",
},
}
// Multiple instances of the servcice with the same name can be created.
// Requests to a service with the same name will be load-balanced.
for i := 0; i < 5; i++ {
svc, err := AddService(nc, config)
if err != nil {
log.Fatal(err)
}
defer svc.Stop()
}
// send a request to a service
resp, err := nc.Request("numbers.increment", []byte("3"), 1*time.Second)
if err != nil {
log.Fatal(err)
}
responseVal, err := strconv.Atoi(string(resp.Data))
if err != nil {
log.Fatal(err)
}
fmt.Println(responseVal)
//
// Output: 4
//
}

267
micro/example_test.go Normal file
View File

@@ -0,0 +1,267 @@
// Copyright 2022 The NATS Authors
// 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.
package micro
import (
"fmt"
"log"
"reflect"
"github.com/nats-io/nats.go"
)
func ExampleAddService() {
nc, err := nats.Connect("127.0.0.1:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
echoHandler := func(req *Request) error {
req.Respond(req.Data)
return nil
}
config := Config{
Name: "EchoService",
Version: "v1.0.0",
Description: "Send back what you receive",
Endpoint: Endpoint{
Subject: "echo",
Handler: echoHandler,
},
// DoneHandler can be set to customize behavior on stopping a service.
DoneHandler: func(srv Service) {
info := srv.Info()
fmt.Printf("stopped service %q with ID %q\n", info.Name, info.ID)
},
// ErrorHandler can be used to customize behavior on service execution error.
ErrorHandler: func(srv Service, err *NATSError) {
info := srv.Info()
fmt.Printf("Service %q returned an error on subject %q: %s", info.Name, err.Subject, err.Description)
},
}
srv, err := AddService(nc, config)
if err != nil {
log.Fatal(err)
}
defer srv.Stop()
}
func ExampleService_Info() {
nc, err := nats.Connect("127.0.0.1:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
config := Config{
Name: "EchoService",
Endpoint: Endpoint{
Subject: "echo",
Handler: func(*Request) error { return nil },
},
}
srv, _ := AddService(nc, config)
// service info
info := srv.Info()
fmt.Println(info.ID)
fmt.Println(info.Name)
fmt.Println(info.Description)
fmt.Println(info.Version)
fmt.Println(info.Subject)
}
func ExampleService_Stats() {
nc, err := nats.Connect("127.0.0.1:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
config := Config{
Name: "EchoService",
Version: "0.1.0",
Endpoint: Endpoint{
Subject: "echo",
Handler: func(*Request) error { return nil },
},
}
srv, _ := AddService(nc, config)
// stats of a service instance
stats := srv.Stats()
fmt.Println(stats.AverageProcessingTime)
fmt.Println(stats.ProcessingTime)
}
func ExampleService_Stop() {
nc, err := nats.Connect("127.0.0.1:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
config := Config{
Name: "EchoService",
Version: "0.1.0",
Endpoint: Endpoint{
Subject: "echo",
Handler: func(*Request) error { return nil },
},
}
srv, _ := AddService(nc, config)
// stop a service
err = srv.Stop()
if err != nil {
log.Fatal(err)
}
// stop is idempotent so multiple executions will not return an error
err = srv.Stop()
if err != nil {
log.Fatal(err)
}
}
func ExampleService_Stopped() {
nc, err := nats.Connect("127.0.0.1:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
config := Config{
Name: "EchoService",
Version: "0.1.0",
Endpoint: Endpoint{
Subject: "echo",
Handler: func(*Request) error { return nil },
},
}
srv, _ := AddService(nc, config)
// stop a service
err = srv.Stop()
if err != nil {
log.Fatal(err)
}
if srv.Stopped() {
fmt.Println("service stopped")
}
}
func ExampleService_Reset() {
nc, err := nats.Connect("127.0.0.1:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
config := Config{
Name: "EchoService",
Version: "0.1.0",
Endpoint: Endpoint{
Subject: "echo",
Handler: func(*Request) error { return nil },
},
}
srv, _ := AddService(nc, config)
// reset endpoint stats on this service
srv.Reset()
empty := Stats{
ServiceIdentity: srv.Info().ServiceIdentity,
}
if !reflect.DeepEqual(srv.Stats(), empty) {
log.Fatal("Expected endpoint stats to be empty")
}
}
func ExampleControlSubject() {
// subject used to get PING from all services
subjectPINGAll, _ := ControlSubject(PingVerb, "", "")
fmt.Println(subjectPINGAll)
// subject used to get PING from services with provided name
subjectPINGName, _ := ControlSubject(PingVerb, "CoolService", "")
fmt.Println(subjectPINGName)
// subject used to get PING from a service with provided name and ID
subjectPINGInstance, _ := ControlSubject(PingVerb, "CoolService", "123")
fmt.Println(subjectPINGInstance)
// Output:
// $SRV.PING
// $SRV.PING.COOLSERVICE
// $SRV.PING.COOLSERVICE.123
}
func ExampleRequest_Respond() {
handler := func(req *Request) {
// respond to the request
if err := req.Respond(req.Data); err != nil {
log.Fatal(err)
}
}
fmt.Printf("%T", handler)
}
func ExampleRequest_RespondJSON() {
type Point struct {
X int `json:"x"`
Y int `json:"y"`
}
handler := func(req *Request) {
resp := Point{5, 10}
// respond to the request
// response will be serialized to {"x":5,"y":10}
if err := req.RespondJSON(resp); err != nil {
log.Fatal(err)
}
}
fmt.Printf("%T", handler)
}
func ExampleRequest_Error() {
handler := func(req *Request) error {
// respond with an error
// Error sets Nats-Service-Error and Nats-Service-Error-Code headers in the response
if err := req.Error("400", "bad request", []byte(`{"error": "value should be a number"}`)); err != nil {
return err
}
return nil
}
fmt.Printf("%T", handler)
}

79
micro/request.go Normal file
View File

@@ -0,0 +1,79 @@
// Copyright 2022 The NATS Authors
// 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.
package micro
import (
"encoding/json"
"errors"
"fmt"
"github.com/nats-io/nats.go"
)
type (
Request struct {
*nats.Msg
}
// RequestHandler is a function used as a Handler for a service.
// It takes a request, which contains the data (payload and headers) of the request,
// as well as exposes methods to respond to the request.
//
// RequestHandler returns an error - if returned, the request will be accounted form in stats (in num_requests),
// and last_error will be set with the value.
RequestHandler func(*Request) error
)
var (
ErrRespond = errors.New("NATS error when sending response")
ErrMarshalResponse = errors.New("marshaling response")
ErrArgRequired = errors.New("argument required")
)
func (r *Request) Respond(response []byte) error {
if err := r.Msg.Respond(response); err != nil {
return fmt.Errorf("%w: %s", ErrRespond, err)
}
return nil
}
func (r *Request) RespondJSON(response interface{}) error {
resp, err := json.Marshal(response)
if err != nil {
return ErrMarshalResponse
}
return r.Respond(resp)
}
// Error prepares and publishes error response from a handler.
// A response error should be set containing an error code and description.
// Optionally, data can be set as response payload.
func (r *Request) Error(code, description string, data []byte) error {
if code == "" {
return fmt.Errorf("%w: error code", ErrArgRequired)
}
if description == "" {
return fmt.Errorf("%w: description", ErrArgRequired)
}
response := &nats.Msg{
Header: nats.Header{
ErrorHeader: []string{description},
ErrorCodeHeader: []string{code},
},
}
response.Data = data
return r.RespondMsg(response)
}

592
micro/service.go Normal file
View File

@@ -0,0 +1,592 @@
// Copyright 2022 The NATS Authors
// 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.
package micro
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nuid"
)
// Notice: Experimental Preview
//
// This functionality is EXPERIMENTAL and may be changed in later releases.
type (
Service interface {
// Info returns the service info.
Info() Info
// Stats returns statisctics for the service endpoint and all monitoring endpoints.
Stats() Stats
// Reset resets all statistics on a service instance.
Reset()
// Stop drains the endpoint subscriptions and marks the service as stopped.
Stop() error
// Stopped informs whether [Stop] was executed on the service.
Stopped() bool
}
// ErrHandler is a function used to configure a custom error handler for a service,
ErrHandler func(Service, *NATSError)
// DoneHandler is a function used to configure a custom done handler for a service.
DoneHandler func(Service)
// StatsHandleris a function used to configure a custom STATS endpoint.
// It should return a value which can be serialized to JSON.
StatsHandler func(Endpoint) interface{}
// ServiceIdentity contains fields helping to identidy a service instance.
ServiceIdentity struct {
Name string `json:"name"`
ID string `json:"id"`
Version string `json:"version"`
}
// Stats is the type returned by STATS monitoring endpoint.
// It contains stats for a specific endpoint (either request handler or monitoring enpoints).
Stats struct {
ServiceIdentity
NumRequests int `json:"num_requests"`
NumErrors int `json:"num_errors"`
LastError string `json:"last_error"`
ProcessingTime time.Duration `json:"processing_time"`
AverageProcessingTime time.Duration `json:"average_processing_time"`
Started string `json:"started"`
Data json.RawMessage `json:"data,omitempty"`
}
// Ping is the response type for PING monitoring endpoint.
Ping ServiceIdentity
// Info is the basic information about a service type.
Info struct {
ServiceIdentity
Description string `json:"description"`
Subject string `json:"subject"`
}
// SchemaResp is the response value for SCHEMA requests.
SchemaResp struct {
ServiceIdentity
Schema Schema `json:"schema"`
}
// Schema can be used to configure a schema for a service.
// It is olso returned by the SCHEMA monitoring service (if set).
Schema struct {
Request string `json:"request"`
Response string `json:"response"`
}
// Endpoint is used to configure a subject and handler for a service.
Endpoint struct {
Subject string `json:"subject"`
Handler RequestHandler
}
// Verb represents a name of the monitoring service.
Verb int64
// Config is a configuration of a service.
Config struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
Schema Schema `json:"schema"`
Endpoint Endpoint `json:"endpoint"`
StatsHandler StatsHandler
DoneHandler DoneHandler
ErrorHandler ErrHandler
}
// NATSError represents an error returned by a NATS Subscription.
// It contains a subject on which the subscription failed, so that
// it can be linked with a specific service endpoint.
NATSError struct {
Subject string
Description string
}
// service represents a configured NATS service.
// It should be created using [Add] in order to configure the appropriate NATS subscriptions
// for request handler and monitoring.
service struct {
// Config contains a configuration of the service
Config
m sync.Mutex
id string
reqSub *nats.Subscription
verbSubs map[string]*nats.Subscription
stats *Stats
conn *nats.Conn
natsHandlers handlers
stopped bool
asyncDispatcher asyncCallbacksHandler
}
handlers struct {
closed nats.ConnHandler
asyncErr nats.ErrHandler
}
asyncCallbacksHandler struct {
cbQueue chan func()
}
)
const (
// Queue Group name used across all services
QG = "q"
// APIPrefix is the root of all control subjects
APIPrefix = "$SRV"
)
// Service Error headers
const (
ErrorHeader = "Nats-Service-Error"
ErrorCodeHeader = "Nats-Service-Error-Code"
)
// Verbs being used to set up a specific control subject.
const (
PingVerb Verb = iota
StatsVerb
InfoVerb
SchemaVerb
)
var (
// this regular expression is suggested regexp for semver validation: https://semver.org/
semVerRegexp = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
serviceNameRegexp = regexp.MustCompile(`^[A-Za-z0-9\-_]+$`)
)
// Common errors returned by the Service framework.
var (
// ErrConfigValidation is returned when service configuration is invalid
ErrConfigValidation = errors.New("validation")
// ErrVerbNotSupported is returned when invalid [Verb] is used (PING, SCHEMA, INFO, STATS)
ErrVerbNotSupported = errors.New("unsupported verb")
// ErrServiceNameRequired is returned when attempting to generate control subject with ID but empty name
ErrServiceNameRequired = errors.New("service name is required to generate ID control subject")
)
func (s Verb) String() string {
switch s {
case PingVerb:
return "PING"
case StatsVerb:
return "STATS"
case InfoVerb:
return "INFO"
case SchemaVerb:
return "SCHEMA"
default:
return ""
}
}
// AddService adds a microservice.
// It will enable internal common services (PING, STATS, INFO and SCHEMA) as well as
// the actual service handler on the subject provided in config.Endpoint
// A service name, version and Endpoint configuration are required to add a service.
// AddService returns a [Service] interface, allowing service menagement.
// Each service is assigned a unique ID.
func AddService(nc *nats.Conn, config Config) (Service, error) {
if err := config.valid(); err != nil {
return nil, err
}
id := nuid.Next()
svc := &service{
Config: config,
conn: nc,
id: id,
asyncDispatcher: asyncCallbacksHandler{
cbQueue: make(chan func(), 100),
},
}
svcIdentity := ServiceIdentity{
Name: config.Name,
ID: id,
Version: config.Version,
}
svc.verbSubs = make(map[string]*nats.Subscription)
svc.stats = &Stats{
ServiceIdentity: svcIdentity,
}
svc.setupAsyncCallbacks()
go svc.asyncDispatcher.asyncCBDispatcher()
// Setup internal subscriptions.
var err error
svc.reqSub, err = nc.QueueSubscribe(config.Endpoint.Subject, QG, func(m *nats.Msg) {
svc.reqHandler(&Request{Msg: m})
})
if err != nil {
svc.asyncDispatcher.close()
return nil, err
}
ping := Ping(svcIdentity)
infoHandler := func(req *Request) error {
response, _ := json.Marshal(svc.Info())
if err := req.Respond(response); err != nil {
if err := req.Error("500", fmt.Sprintf("Error handling INFO request: %s", err), nil); err != nil && config.ErrorHandler != nil {
svc.asyncDispatcher.push(func() { config.ErrorHandler(svc, &NATSError{req.Subject, err.Error()}) })
}
}
return nil
}
pingHandler := func(req *Request) error {
response, _ := json.Marshal(ping)
if err := req.Respond(response); err != nil {
if err := req.Error("500", fmt.Sprintf("Error handling PING request: %s", err), nil); err != nil && config.ErrorHandler != nil {
svc.asyncDispatcher.push(func() { config.ErrorHandler(svc, &NATSError{req.Subject, err.Error()}) })
}
}
return nil
}
statsHandler := func(req *Request) error {
response, _ := json.Marshal(svc.Stats())
if err := req.Respond(response); err != nil {
if err := req.Error("500", fmt.Sprintf("Error handling STATS request: %s", err), nil); err != nil && config.ErrorHandler != nil {
svc.asyncDispatcher.push(func() { config.ErrorHandler(svc, &NATSError{req.Subject, err.Error()}) })
}
}
return nil
}
schema := SchemaResp{
ServiceIdentity: svcIdentity,
Schema: config.Schema,
}
schemaHandler := func(req *Request) error {
response, _ := json.Marshal(schema)
if err := req.Respond(response); err != nil {
if err := req.Error("500", fmt.Sprintf("Error handling SCHEMA request: %s", err), nil); err != nil && config.ErrorHandler != nil {
svc.asyncDispatcher.push(func() { config.ErrorHandler(svc, &NATSError{req.Subject, err.Error()}) })
}
}
return nil
}
if err := svc.verbHandlers(nc, InfoVerb, infoHandler); err != nil {
svc.asyncDispatcher.close()
return nil, err
}
if err := svc.verbHandlers(nc, PingVerb, pingHandler); err != nil {
svc.asyncDispatcher.close()
return nil, err
}
if err := svc.verbHandlers(nc, StatsVerb, statsHandler); err != nil {
svc.asyncDispatcher.close()
return nil, err
}
if err := svc.verbHandlers(nc, SchemaVerb, schemaHandler); err != nil {
svc.asyncDispatcher.close()
return nil, err
}
svc.stats.Started = time.Now().Format(time.RFC3339)
return svc, nil
}
// dispatch is responsible for calling any async callbacks
func (ac *asyncCallbacksHandler) asyncCBDispatcher() {
for {
f := <-ac.cbQueue
if f == nil {
return
}
f()
}
}
// dispatch is responsible for calling any async callbacks
func (ac *asyncCallbacksHandler) push(f func()) {
ac.cbQueue <- f
}
func (ac *asyncCallbacksHandler) close() {
close(ac.cbQueue)
}
func (s *Config) valid() error {
if !serviceNameRegexp.MatchString(s.Name) {
return fmt.Errorf("%w: service name: name should not be empty and should consist of alphanumerical charactest, dashes and underscores", ErrConfigValidation)
}
if !semVerRegexp.MatchString(s.Version) {
return fmt.Errorf("%w: version: version should not be empty should match the SemVer format", ErrConfigValidation)
}
return s.Endpoint.valid()
}
func (e *Endpoint) valid() error {
if e.Subject == "" {
return fmt.Errorf("%w: endpoint: subject is required", ErrConfigValidation)
}
if e.Handler == nil {
return fmt.Errorf("%w: endpoint: handler is required", ErrConfigValidation)
}
return nil
}
func (svc *service) setupAsyncCallbacks() {
svc.natsHandlers.closed = svc.conn.ClosedHandler()
if svc.natsHandlers.closed != nil {
svc.conn.SetClosedHandler(func(c *nats.Conn) {
svc.Stop()
svc.natsHandlers.closed(c)
})
} else {
svc.conn.SetClosedHandler(func(c *nats.Conn) {
svc.Stop()
})
}
svc.natsHandlers.asyncErr = svc.conn.ErrorHandler()
if svc.natsHandlers.asyncErr != nil {
svc.conn.SetErrorHandler(func(c *nats.Conn, s *nats.Subscription, err error) {
if !svc.matchSubscriptionSubject(s.Subject) {
svc.natsHandlers.asyncErr(c, s, err)
}
if svc.Config.ErrorHandler != nil {
svc.Config.ErrorHandler(svc, &NATSError{
Subject: s.Subject,
Description: err.Error(),
})
}
svc.Stop()
svc.natsHandlers.asyncErr(c, s, err)
})
} else {
svc.conn.SetErrorHandler(func(c *nats.Conn, s *nats.Subscription, err error) {
if !svc.matchSubscriptionSubject(s.Subject) {
return
}
if svc.Config.ErrorHandler != nil {
svc.Config.ErrorHandler(svc, &NATSError{
Subject: s.Subject,
Description: err.Error(),
})
}
svc.Stop()
})
}
}
func (svc *service) matchSubscriptionSubject(subj string) bool {
if svc.reqSub.Subject == subj {
return true
}
for _, verbSub := range svc.verbSubs {
if verbSub.Subject == subj {
return true
}
}
return false
}
// verbHandlers generates control handlers for a specific verb.
// Each request generates 3 subscriptions, one for the general verb
// affecting all services written with the framework, one that handles
// all services of a particular kind, and finally a specific service instance.
func (svc *service) verbHandlers(nc *nats.Conn, verb Verb, handler RequestHandler) error {
name := fmt.Sprintf("%s-all", verb.String())
if err := svc.addInternalHandler(nc, verb, "", "", name, handler); err != nil {
return err
}
name = fmt.Sprintf("%s-kind", verb.String())
if err := svc.addInternalHandler(nc, verb, svc.Config.Name, "", name, handler); err != nil {
return err
}
return svc.addInternalHandler(nc, verb, svc.Config.Name, svc.id, verb.String(), handler)
}
// addInternalHandler registers a control subject handler.
func (s *service) addInternalHandler(nc *nats.Conn, verb Verb, kind, id, name string, handler RequestHandler) error {
subj, err := ControlSubject(verb, kind, id)
if err != nil {
s.Stop()
return err
}
s.verbSubs[name], err = nc.Subscribe(subj, func(msg *nats.Msg) {
handler(&Request{Msg: msg})
})
if err != nil {
s.Stop()
return err
}
return nil
}
// reqHandler itself
func (s *service) reqHandler(req *Request) {
start := time.Now()
err := s.Endpoint.Handler(req)
s.m.Lock()
s.stats.NumRequests++
s.stats.ProcessingTime += time.Since(start)
avgProcessingTime := s.stats.ProcessingTime.Nanoseconds() / int64(s.stats.NumRequests)
s.stats.AverageProcessingTime = time.Duration(avgProcessingTime)
if err != nil {
s.stats.NumErrors++
s.stats.LastError = err.Error()
}
s.m.Unlock()
}
// Stop drains the endpoint subscriptions and marks the service as stopped.
func (s *service) Stop() error {
s.m.Lock()
if s.stopped {
return nil
}
defer s.m.Unlock()
if s.reqSub != nil {
if err := s.reqSub.Drain(); err != nil {
return fmt.Errorf("draining subscription for request handler: %w", err)
}
s.reqSub = nil
}
var keys []string
for key, sub := range s.verbSubs {
keys = append(keys, key)
if err := sub.Drain(); err != nil {
return fmt.Errorf("draining subscription for subject %q: %w", sub.Subject, err)
}
}
for _, key := range keys {
delete(s.verbSubs, key)
}
restoreAsyncHandlers(s.conn, s.natsHandlers)
s.stopped = true
if s.DoneHandler != nil {
s.asyncDispatcher.push(func() { s.DoneHandler(s) })
s.asyncDispatcher.close()
}
return nil
}
func restoreAsyncHandlers(nc *nats.Conn, handlers handlers) {
nc.SetClosedHandler(handlers.closed)
nc.SetErrorHandler(handlers.asyncErr)
}
// ID returns the service instance's unique ID.
func (s *service) Info() Info {
return Info{
ServiceIdentity: ServiceIdentity{
Name: s.Config.Name,
ID: s.id,
Version: s.Config.Version,
},
Description: s.Config.Description,
Subject: s.Config.Endpoint.Subject,
}
}
// Stats returns statisctics for the service endpoint and all monitoring endpoints.
func (s *service) Stats() Stats {
s.m.Lock()
defer s.m.Unlock()
if s.StatsHandler != nil {
s.stats.Data, _ = json.Marshal(s.StatsHandler(s.Endpoint))
}
info := s.Info()
return Stats{
ServiceIdentity: ServiceIdentity{
Name: info.Name,
ID: info.ID,
Version: info.Version,
},
NumRequests: s.stats.NumRequests,
NumErrors: s.stats.NumErrors,
ProcessingTime: s.stats.ProcessingTime,
AverageProcessingTime: s.stats.AverageProcessingTime,
Started: s.stats.Started,
Data: s.stats.Data,
}
}
// Reset resets all statistics on a service instance.
func (s *service) Reset() {
s.m.Lock()
s.stats = &Stats{
ServiceIdentity: s.Info().ServiceIdentity,
}
s.m.Unlock()
}
// Stopped informs whether [Stop] was executed on the service.
func (s *service) Stopped() bool {
s.m.Lock()
defer s.m.Unlock()
return s.stopped
}
// ControlSubject returns monitoring subjects used by the Service.
// Providing a verb is mandatory (it should be one of Ping, Schema, Info or Stats).
// Depending on whether kind and id are provided, ControlSubject will return one of the following:
// - verb only: subject used to monitor all available services
// - verb and kind: subject used to monitor services with the provided name
// - verb, name and id: subject used to monitor an instance of a service with the provided ID
func ControlSubject(verb Verb, name, id string) (string, error) {
verbStr := verb.String()
if verbStr == "" {
return "", fmt.Errorf("%w: %q", ErrVerbNotSupported, verbStr)
}
if name == "" && id != "" {
return "", ErrServiceNameRequired
}
name = strings.ToUpper(name)
if name == "" && id == "" {
return fmt.Sprintf("%s.%s", APIPrefix, verbStr), nil
}
if id == "" {
return fmt.Sprintf("%s.%s.%s", APIPrefix, verbStr, name), nil
}
return fmt.Sprintf("%s.%s.%s.%s", APIPrefix, verbStr, name, id), nil
}
func (e *NATSError) Error() string {
return fmt.Sprintf("%q: %s", e.Subject, e.Description)
}

1122
micro/service_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2013-2018 The NATS Authors // Copyright 2013-2022 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View File

@@ -1,4 +1,4 @@
// Copyright 2012-2020 The NATS Authors // Copyright 2012-2122 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View File

@@ -1,25 +0,0 @@
// Copyright 2022 The NATS Authors
// 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.
package services
import "fmt"
type ServiceAPIError struct {
ErrorCode int
Description string
}
func (e *ServiceAPIError) Error() string {
return fmt.Sprintf("%d %s", e.ErrorCode, e.Description)
}

View File

@@ -1,404 +0,0 @@
// Copyright 2022 The NATS Authors
// 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.
package services
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nuid"
)
// Notice: Experimental Preview
//
// This functionality is EXPERIMENTAL and may be changed in later releases.
type (
// Service is an interface for service management.
// It exposes methods to stop/reset a service, as well as get information on a service.
Service interface {
ID() string
Name() string
Description() string
Version() string
Stats() ServiceStats
Reset()
Stop()
}
// A request handler.
// TODO (could make error more and return more info to user automatically?)
ServiceHandler func(svc Service, req *nats.Msg) error
// Clients can request as well.
ServiceStats struct {
Name string `json:"name"`
ID string `json:"id"`
Version string `json:"version"`
Started time.Time `json:"started"`
Endpoints []Stats `json:"stats"`
}
Stats struct {
Name string `json:"name"`
NumRequests int `json:"num_requests"`
NumErrors int `json:"num_errors"`
TotalLatency time.Duration `json:"total_latency"`
AverageLatency time.Duration `json:"average_latency"`
Data interface{} `json:"data"`
}
// ServiceInfo is the basic information about a service type
ServiceInfo struct {
Name string `json:"name"`
ID string `json:"id"`
Description string `json:"description"`
Version string `json:"version"`
Subject string `json:"subject"`
}
ServiceSchema struct {
Request string `json:"request"`
Response string `json:"response"`
}
Endpoint struct {
Subject string `json:"subject"`
Handler ServiceHandler
}
InternalEndpoint struct {
Name string
Handler nats.MsgHandler
}
ServiceVerb int64
ServiceConfig struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Schema ServiceSchema `json:"schema"`
Endpoint Endpoint `json:"endpoint"`
StatusHandler func(Endpoint) interface{}
}
// service is the internal implementation of a Service
service struct {
sync.Mutex
ServiceConfig
id string
// subs
reqSub *nats.Subscription
internal map[string]*nats.Subscription
statuses map[string]*Stats
stats *ServiceStats
conn *nats.Conn
}
)
const (
// We can fix this, as versions will be on separate subjects and use account mapping to roll requests to new versions etc.
QG = "svc"
// ServiceApiPrefix is the root of all control subjects
ServiceApiPrefix = "$SRV"
ServiceErrorHeader = "Nats-Service-Error"
)
const (
SrvPing ServiceVerb = iota
SrvStatus
SrvInfo
SrvSchema
)
func (s *ServiceConfig) Valid() error {
if s.Name == "" {
return errors.New("name is required")
}
return s.Endpoint.Valid()
}
func (e *Endpoint) Valid() error {
s := strings.TrimSpace(e.Subject)
if len(s) == 0 {
return errors.New("subject is required")
}
if e.Handler == nil {
return errors.New("handler is required")
}
return nil
}
func (s ServiceVerb) String() string {
switch s {
case SrvPing:
return "PING"
case SrvStatus:
return "STATUS"
case SrvInfo:
return "INFO"
case SrvSchema:
return "SCHEMA"
default:
return ""
}
}
// Add adds a microservice.
// NOTE we can do an OpenAPI version as well, but looking at it it was very involved. So I think keep simple version and
// also have a version that talkes full blown OpenAPI spec and we can pull these things out.
func Add(nc *nats.Conn, config ServiceConfig) (Service, error) {
if err := config.Valid(); err != nil {
return nil, err
}
id := nuid.Next()
svc := &service{
ServiceConfig: config,
conn: nc,
id: id,
}
svc.internal = make(map[string]*nats.Subscription)
svc.statuses = make(map[string]*Stats)
svc.statuses[""] = &Stats{
Name: config.Name,
}
svc.stats = &ServiceStats{
Name: config.Name,
ID: id,
Version: config.Version,
Started: time.Now(),
}
// Setup internal subscriptions.
var err error
svc.reqSub, err = nc.QueueSubscribe(config.Endpoint.Subject, QG, func(m *nats.Msg) {
svc.reqHandler(m)
})
if err != nil {
return nil, err
}
info := &ServiceInfo{
Name: config.Name,
ID: id,
Description: config.Description,
Version: config.Version,
Subject: config.Endpoint.Subject,
}
infoHandler := func(m *nats.Msg) {
response, _ := json.MarshalIndent(info, "", " ")
m.Respond(response)
}
pingHandler := func(m *nats.Msg) {
infoHandler(m)
}
statusHandler := func(m *nats.Msg) {
response, _ := json.MarshalIndent(svc.Stats(), "", " ")
m.Respond(response)
}
schemaHandler := func(m *nats.Msg) {
response, _ := json.MarshalIndent(svc.ServiceConfig.Schema, "", " ")
m.Respond(response)
}
if err := svc.addInternalHandlerGroup(nc, SrvInfo, infoHandler); err != nil {
return nil, err
}
if err := svc.addInternalHandlerGroup(nc, SrvPing, pingHandler); err != nil {
return nil, err
}
if err := svc.addInternalHandlerGroup(nc, SrvStatus, statusHandler); err != nil {
return nil, err
}
if svc.ServiceConfig.Schema.Request != "" || svc.ServiceConfig.Schema.Response != "" {
if err := svc.addInternalHandlerGroup(nc, SrvSchema, schemaHandler); err != nil {
return nil, err
}
}
svc.stats.ID = id
svc.stats.Started = time.Now()
return svc, nil
}
// addInternalHandlerGroup generates control handlers for a specific verb
// each request generates 3 subscriptions, one for the general verb
// affecting all services written with the framework, one that handles
// all services of a particular kind, and finally a specific service.
func (svc *service) addInternalHandlerGroup(nc *nats.Conn, verb ServiceVerb, handler nats.MsgHandler) error {
name := fmt.Sprintf("%s-all", verb.String())
if err := svc.addInternalHandler(nc, verb, "", "", name, handler); err != nil {
return err
}
name = fmt.Sprintf("%s-kind", verb.String())
if err := svc.addInternalHandler(nc, verb, svc.Name(), "", name, handler); err != nil {
return err
}
return svc.addInternalHandler(nc, verb, svc.Name(), svc.ID(), verb.String(), handler)
}
// addInternalHandler registers a control subject handler
func (svc *service) addInternalHandler(nc *nats.Conn, verb ServiceVerb, kind, id, name string, handler nats.MsgHandler) error {
subj, err := SvcControlSubject(verb, kind, id)
if err != nil {
svc.Stop()
return err
}
svc.internal[name], err = nc.Subscribe(subj, func(msg *nats.Msg) {
start := time.Now()
defer func() {
svc.Lock()
stats := svc.statuses[name]
stats.NumRequests++
stats.TotalLatency += time.Since(start)
stats.AverageLatency = stats.TotalLatency / time.Duration(stats.NumRequests)
svc.Unlock()
}()
handler(msg)
})
if err != nil {
svc.Stop()
return err
}
svc.statuses[name] = &Stats{
Name: name,
}
return nil
}
// reqHandler itself
func (svc *service) reqHandler(req *nats.Msg) {
start := time.Now()
defer func() {
svc.Lock()
stats := svc.statuses[""]
stats.NumRequests++
stats.TotalLatency += time.Since(start)
stats.AverageLatency = stats.TotalLatency / time.Duration(stats.NumRequests)
svc.Unlock()
}()
if err := svc.ServiceConfig.Endpoint.Handler(svc, req); err != nil {
hdr := make(nats.Header)
apiErr := &ServiceAPIError{}
if ok := errors.As(err, &apiErr); !ok {
hdr[ServiceErrorHeader] = []string{fmt.Sprintf("%d %s", 500, err.Error())}
} else {
hdr[ServiceErrorHeader] = []string{apiErr.Error()}
}
svc.Lock()
stats := svc.statuses[""]
stats.NumErrors++
svc.Unlock()
svc.conn.PublishMsg(&nats.Msg{
Subject: req.Reply,
Header: hdr,
})
}
}
func (svc *service) Stop() {
if svc.reqSub != nil {
svc.reqSub.Drain()
svc.reqSub = nil
}
var keys []string
for key, sub := range svc.internal {
keys = append(keys, key)
sub.Drain()
}
for _, key := range keys {
delete(svc.internal, key)
}
}
func (svc *service) ID() string {
return svc.id
}
func (svc *service) Name() string {
return svc.ServiceConfig.Name
}
func (svc *service) Description() string {
return svc.ServiceConfig.Description
}
func (svc *service) Version() string {
return svc.ServiceConfig.Version
}
func (svc *service) Stats() ServiceStats {
svc.Lock()
defer func() {
svc.Unlock()
}()
if svc.ServiceConfig.StatusHandler != nil {
stats := svc.statuses[""]
stats.Data = svc.ServiceConfig.StatusHandler(svc.Endpoint)
}
idx := 0
v := make([]Stats, len(svc.statuses))
for _, se := range svc.statuses {
v[idx] = *se
idx++
}
svc.stats.Endpoints = v
return *svc.stats
}
func (svc *service) Reset() {
for _, se := range svc.statuses {
se.NumRequests = 0
se.TotalLatency = 0
se.NumErrors = 0
se.Data = nil
}
}
// SvcControlSubject returns monitoring subjects used by the ServiceImpl
func SvcControlSubject(verb ServiceVerb, kind, id string) (string, error) {
sverb := verb.String()
if sverb == "" {
return "", fmt.Errorf("unsupported service verb")
}
kind = strings.ToUpper(kind)
if kind == "" && id == "" {
return fmt.Sprintf("%s.%s", ServiceApiPrefix, sverb), nil
}
if id == "" {
return fmt.Sprintf("%s.%s.%s", ServiceApiPrefix, sverb, kind), nil
}
return fmt.Sprintf("%s.%s.%s.%s", ServiceApiPrefix, sverb, kind, id), nil
}

View File

@@ -1,260 +0,0 @@
// Copyright 2022 The NATS Authors
// 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.
package services
import (
"encoding/json"
"fmt"
"math/rand"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
natsserver "github.com/nats-io/nats-server/v2/test"
"github.com/nats-io/nats.go"
)
func TestServiceBasics(t *testing.T) {
s := RunServerOnPort(-1)
defer s.Shutdown()
nc, err := nats.Connect(s.ClientURL())
if err != nil {
t.Fatalf("Expected to connect to server, got %v", err)
}
defer nc.Close()
// Stub service.
doAdd := func(svc Service, req *nats.Msg) error {
if rand.Intn(10) == 0 {
return fmt.Errorf("Unexpected Error!")
}
// Happy Path.
// Random delay between 5-10ms
time.Sleep(5*time.Millisecond + time.Duration(rand.Intn(5))*time.Millisecond)
if err := req.Respond([]byte("42")); err != nil {
return err
}
return nil
}
var svcs []Service
// Create 5 service responders.
config := ServiceConfig{
Name: "CoolAddService",
Version: "v0.1",
Description: "Add things together",
Endpoint: Endpoint{
Subject: "svc.add",
Handler: doAdd,
},
Schema: ServiceSchema{Request: "", Response: ""},
}
for i := 0; i < 5; i++ {
svc, err := Add(nc, config)
if err != nil {
t.Fatalf("Expected to create Service, got %v", err)
}
defer svc.Stop()
svcs = append(svcs, svc)
}
// Now send 50 requests.
for i := 0; i < 50; i++ {
_, err := nc.Request("svc.add", []byte(`{ "x": 22, "y": 11 }`), time.Second)
if err != nil {
t.Fatalf("Expected a response, got %v", err)
}
}
for _, svc := range svcs {
if svc.Name() != "CoolAddService" {
t.Fatalf("Expected %q, got %q", "CoolAddService", svc.Name())
}
if len(svc.Description()) == 0 || len(svc.Version()) == 0 {
t.Fatalf("Expected non empty description and version")
}
}
// Make sure we can request info, 1 response.
// This could be exported as well as main ServiceImpl.
subj, err := SvcControlSubject(SrvInfo, "CoolAddService", "")
if err != nil {
t.Fatalf("Failed to building info subject %v", err)
}
info, err := nc.Request(subj, nil, time.Second)
if err != nil {
t.Fatalf("Expected a response, got %v", err)
}
var inf ServiceInfo
if err := json.Unmarshal(info.Data, &inf); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if inf.Subject != "svc.add" {
t.Fatalf("expected service subject to be srv.add: %s", inf.Subject)
}
// Ping all services. Multiple responses.
// could do STATZ too?
inbox := nats.NewInbox()
sub, err := nc.SubscribeSync(inbox)
if err != nil {
t.Fatalf("subscribe failed: %s", err)
}
pingSubject, err := SvcControlSubject(SrvPing, "CoolAddService", "")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if err := nc.PublishRequest(pingSubject, inbox, nil); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
var pingCount int
for {
_, err := sub.NextMsg(250 * time.Millisecond)
if err != nil {
break
}
pingCount++
}
if pingCount != 5 {
t.Fatalf("Expected 5 ping responses, got: %d", pingCount)
}
// Get stats from all services
statsInbox := nats.NewInbox()
sub, err = nc.SubscribeSync(statsInbox)
if err != nil {
t.Fatalf("subscribe failed: %s", err)
}
statsSubject, err := SvcControlSubject(SrvStatus, "CoolAddService", "")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if err := nc.PublishRequest(statsSubject, statsInbox, nil); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
stats := make([]ServiceStats, 0)
var requestsNum int
for {
resp, err := sub.NextMsg(250 * time.Millisecond)
if err != nil {
break
}
var srvStats ServiceStats
if err := json.Unmarshal(resp.Data, &srvStats); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(srvStats.Endpoints) != 10 {
t.Fatalf("Expected 10 endpoints on a serivce, got: %d", len(srvStats.Endpoints))
}
for _, e := range srvStats.Endpoints {
if e.Name == "CoolAddService" {
requestsNum += e.NumRequests
}
}
stats = append(stats, srvStats)
}
if len(stats) != 5 {
t.Fatalf("Expected stats for 5 services, got: %d", len(stats))
}
// Services should process 50 requests total
if requestsNum != 50 {
t.Fatalf("Expected a total fo 50 requests processed, got: %d", requestsNum)
}
}
func TestServiceErrors(t *testing.T) {
tests := []struct {
name string
handlerResponse error
expectedStatus string
}{
{
name: "generic error",
handlerResponse: fmt.Errorf("oops"),
expectedStatus: "500 oops",
},
{
name: "api error",
handlerResponse: &ServiceAPIError{ErrorCode: 400, Description: "oops"},
expectedStatus: "400 oops",
},
{
name: "no error",
handlerResponse: nil,
expectedStatus: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := RunServerOnPort(-1)
defer s.Shutdown()
nc, err := nats.Connect(s.ClientURL())
if err != nil {
t.Fatalf("Expected to connect to server, got %v", err)
}
defer nc.Close()
// Stub service.
handler := func(svc Service, req *nats.Msg) error {
if test.handlerResponse == nil {
if err := req.Respond([]byte("ok")); err != nil {
return err
}
}
return test.handlerResponse
}
svc, err := Add(nc, ServiceConfig{
Name: "CoolService",
Description: "Erroring service",
Endpoint: Endpoint{
Subject: "svc.fail",
Handler: handler,
},
})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defer svc.Stop()
resp, err := nc.Request("svc.fail", nil, 1*time.Second)
if err != nil {
t.Fatalf("request error")
}
status := resp.Header.Get("Nats-Service-Error")
if status != test.expectedStatus {
t.Fatalf("Invalid response status; want: %q; got: %q", test.expectedStatus, status)
}
})
}
}
func RunServerOnPort(port int) *server.Server {
opts := natsserver.DefaultTestOptions
opts.Port = port
return RunServerWithOptions(&opts)
}
func RunServerWithOptions(opts *server.Options) *server.Server {
return natsserver.RunServer(opts)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2018 The NATS Authors // Copyright 2017-2022 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2018 The NATS Authors // Copyright 2017-2022 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

2
ws.go
View File

@@ -1,4 +1,4 @@
// Copyright 2021 The NATS Authors // Copyright 2021-2022 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at

View File

@@ -1,4 +1,4 @@
// Copyright 2021 The NATS Authors // Copyright 2021-2022 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at