mirror of
https://github.com/nats-io/nats.go.git
synced 2025-09-27 04:46:01 +08:00
[ADDED] Service api improvements (#1160)
Co-authored-by: Tomasz Pietrek <melgaer@gmail.com>
This commit is contained in:
4
js.go
4
js.go
@@ -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
|
||||||
|
84
micro/example_package_test.go
Normal file
84
micro/example_package_test.go
Normal 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
267
micro/example_test.go
Normal 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
79
micro/request.go
Normal 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
592
micro/service.go
Normal 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
1122
micro/service_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -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)
|
|
||||||
}
|
|
2
timer.go
2
timer.go
@@ -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
|
||||||
|
@@ -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
2
ws.go
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user