The controller imports mesh-host/validate through a replace onto the forge that holds it, and every build — the build agent's go build in a fresh toolchain container, the Dockerfile's go mod download — would have fetched it through the public proxy and checksum database at build time: a merge breaking main on the network, the class Phase 1 removes. vendor/ is committed; go builds from it with nothing fetched, and refuses to build when it and go.mod disagree, so a pin moved without go mod vendor fails at once. The Dockerfile copies vendor/ and builds with GOPROXY=off.
1024 lines
28 KiB
Go
1024 lines
28 KiB
Go
// Copyright 2022-2023 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"
|
|
)
|
|
|
|
type (
|
|
|
|
// Service exposes methods to operate on a service instance.
|
|
Service interface {
|
|
// AddEndpoint registers endpoint with given name on a specific subject.
|
|
AddEndpoint(string, Handler, ...EndpointOpt) error
|
|
|
|
// AddGroup returns a Group interface, allowing for more complex endpoint topologies.
|
|
// A group can be used to register endpoints with given prefix.
|
|
AddGroup(string, ...GroupOpt) Group
|
|
|
|
// Info returns the service info.
|
|
Info() Info
|
|
|
|
// Stats returns statistics for the service endpoint and all monitoring endpoints.
|
|
Stats() Stats
|
|
|
|
// Reset resets all statistics (for all endpoints) 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
|
|
}
|
|
|
|
// Group allows for grouping endpoints on a service.
|
|
//
|
|
// Endpoints created using AddEndpoint will be grouped under common prefix (group name)
|
|
// New groups can also be derived from a group using AddGroup.
|
|
Group interface {
|
|
// AddGroup creates a new group, prefixed by this group's prefix.
|
|
AddGroup(string, ...GroupOpt) Group
|
|
|
|
// AddEndpoint registers new endpoints on a service.
|
|
// The endpoint's subject will be prefixed with the group prefix.
|
|
AddEndpoint(string, Handler, ...EndpointOpt) error
|
|
}
|
|
|
|
EndpointOpt func(*endpointOpts) error
|
|
GroupOpt func(*groupOpts)
|
|
|
|
endpointOpts struct {
|
|
subject string
|
|
metadata map[string]string
|
|
queueGroup string
|
|
qgDisabled bool
|
|
msgLimit int
|
|
bytesLimit int
|
|
}
|
|
|
|
groupOpts struct {
|
|
queueGroup string
|
|
qgDisabled 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)
|
|
|
|
// StatsHandler is a function used to configure a custom STATS endpoint.
|
|
// It should return a value which can be serialized to JSON.
|
|
StatsHandler func(*Endpoint) any
|
|
|
|
// ServiceIdentity contains fields helping to identity a service instance.
|
|
ServiceIdentity struct {
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
Version string `json:"version"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
// Stats is the type returned by STATS monitoring endpoint.
|
|
// It contains stats of all registered endpoints.
|
|
Stats struct {
|
|
ServiceIdentity
|
|
Type string `json:"type"`
|
|
Started time.Time `json:"started"`
|
|
Endpoints []*EndpointStats `json:"endpoints"`
|
|
}
|
|
|
|
// EndpointStats contains stats for a specific endpoint.
|
|
EndpointStats struct {
|
|
Name string `json:"name"`
|
|
Subject string `json:"subject"`
|
|
QueueGroup string `json:"queue_group"`
|
|
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"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
// Ping is the response type for PING monitoring endpoint.
|
|
Ping struct {
|
|
ServiceIdentity
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// Info is the basic information about a service type.
|
|
Info struct {
|
|
ServiceIdentity
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
Endpoints []EndpointInfo `json:"endpoints"`
|
|
}
|
|
|
|
EndpointInfo struct {
|
|
Name string `json:"name"`
|
|
Subject string `json:"subject"`
|
|
QueueGroup string `json:"queue_group"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
// Endpoint manages a service endpoint.
|
|
Endpoint struct {
|
|
EndpointConfig
|
|
Name string
|
|
|
|
service *service
|
|
|
|
stats EndpointStats
|
|
subscription *nats.Subscription
|
|
}
|
|
|
|
group struct {
|
|
service *service
|
|
prefix string
|
|
queueGroup string
|
|
queueGroupDisabled bool
|
|
}
|
|
|
|
// Verb represents a name of the monitoring service.
|
|
Verb int64
|
|
|
|
// Config is a configuration of a service.
|
|
Config struct {
|
|
// Name represents the name of the service.
|
|
Name string `json:"name"`
|
|
|
|
// Endpoint is an optional endpoint configuration.
|
|
// More complex, multi-endpoint services can be configured using
|
|
// Service.AddGroup and Service.AddEndpoint methods.
|
|
Endpoint *EndpointConfig `json:"endpoint"`
|
|
|
|
// Version is a SemVer compatible version string.
|
|
Version string `json:"version"`
|
|
|
|
// Description of the service.
|
|
Description string `json:"description"`
|
|
|
|
// Metadata annotates the service
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
|
|
// QueueGroup can be used to override the default queue group name.
|
|
QueueGroup string `json:"queue_group"`
|
|
|
|
// QueueGroupDisabled disables the queue group for the service.
|
|
QueueGroupDisabled bool `json:"queue_group_disabled"`
|
|
|
|
// StatsHandler is a user-defined custom function.
|
|
// used to calculate additional service stats.
|
|
StatsHandler StatsHandler
|
|
|
|
// DoneHandler is invoked when all service subscription are stopped.
|
|
DoneHandler DoneHandler
|
|
|
|
// ErrorHandler is invoked on any nats-related service error.
|
|
ErrorHandler ErrHandler
|
|
}
|
|
|
|
EndpointConfig struct {
|
|
// Subject on which the endpoint is registered.
|
|
Subject string
|
|
|
|
// Handler used by the endpoint.
|
|
Handler Handler
|
|
|
|
// Metadata annotates the service
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
|
|
// QueueGroup can be used to override the default queue group name.
|
|
QueueGroup string `json:"queue_group"`
|
|
|
|
// QueueGroupDisabled disables the queue group for the endpoint.
|
|
QueueGroupDisabled bool `json:"queue_group_disabled"`
|
|
}
|
|
|
|
// 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
|
|
err error
|
|
}
|
|
|
|
// 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
|
|
endpoints []*Endpoint
|
|
verbSubs map[string]*nats.Subscription
|
|
started time.Time
|
|
nc *nats.Conn
|
|
natsHandlers handlers
|
|
stopped bool
|
|
|
|
asyncDispatcher asyncCallbacksHandler
|
|
}
|
|
|
|
handlers struct {
|
|
closed nats.ConnHandler
|
|
asyncErr nats.ErrHandler
|
|
}
|
|
|
|
asyncCallbacksHandler struct {
|
|
cbQueue chan func()
|
|
closed bool
|
|
}
|
|
)
|
|
|
|
const (
|
|
// Queue Group name used across all services
|
|
DefaultQueueGroup = "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
|
|
)
|
|
|
|
const (
|
|
InfoResponseType = "io.nats.micro.v1.info_response"
|
|
PingResponseType = "io.nats.micro.v1.ping_response"
|
|
StatsResponseType = "io.nats.micro.v1.stats_response"
|
|
)
|
|
|
|
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-]+)*))?$`)
|
|
nameRegexp = regexp.MustCompile(`^[A-Za-z0-9\-_]+$`)
|
|
subjectRegexp = regexp.MustCompile(`^[^ >]*[>]?$`)
|
|
)
|
|
|
|
// 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, 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"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// AddService adds a microservice.
|
|
// It will enable internal common services (PING, STATS and INFO).
|
|
// Request handlers have to be registered separately using Service.AddEndpoint.
|
|
// A service name, version and Endpoint configuration are required to add a service.
|
|
// AddService returns a [Service] interface, allowing service management.
|
|
// 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
|
|
}
|
|
|
|
if config.Metadata == nil {
|
|
config.Metadata = map[string]string{}
|
|
}
|
|
|
|
id := nuid.Next()
|
|
svc := &service{
|
|
Config: config,
|
|
nc: nc,
|
|
id: id,
|
|
asyncDispatcher: asyncCallbacksHandler{
|
|
cbQueue: make(chan func(), 100),
|
|
},
|
|
verbSubs: make(map[string]*nats.Subscription),
|
|
endpoints: make([]*Endpoint, 0),
|
|
}
|
|
|
|
// Add connection event (closed, error) wrapper handlers. If the service has
|
|
// custom callbacks, the events are queued and invoked by the same
|
|
// goroutine, starting now.
|
|
go svc.asyncDispatcher.run()
|
|
svc.wrapConnectionEventCallbacks()
|
|
|
|
if config.Endpoint != nil {
|
|
opts := []EndpointOpt{WithEndpointSubject(config.Endpoint.Subject)}
|
|
if config.Endpoint.Metadata != nil {
|
|
opts = append(opts, WithEndpointMetadata(config.Endpoint.Metadata))
|
|
}
|
|
if config.Endpoint.QueueGroup != "" {
|
|
opts = append(opts, WithEndpointQueueGroup(config.Endpoint.QueueGroup))
|
|
} else if config.QueueGroup != "" {
|
|
opts = append(opts, WithEndpointQueueGroup(config.QueueGroup))
|
|
}
|
|
if err := svc.AddEndpoint("default", config.Endpoint.Handler, opts...); err != nil {
|
|
svc.asyncDispatcher.close()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Setup internal subscriptions.
|
|
pingResponse := Ping{
|
|
ServiceIdentity: svc.serviceIdentity(),
|
|
Type: PingResponseType,
|
|
}
|
|
|
|
handleVerb := func(verb Verb, valuef func() any) func(req Request) {
|
|
return func(req Request) {
|
|
response, _ := json.Marshal(valuef())
|
|
if err := req.Respond(response); err != nil {
|
|
if err := req.Error("500", fmt.Sprintf("Error handling %s request: %s", verb, err), nil); err != nil && config.ErrorHandler != nil {
|
|
svc.asyncDispatcher.push(func() {
|
|
config.ErrorHandler(svc, &NATSError{Subject: req.Subject(), Description: err.Error(), err: err})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for verb, source := range map[Verb]func() any{
|
|
InfoVerb: func() any { return svc.Info() },
|
|
PingVerb: func() any { return pingResponse },
|
|
StatsVerb: func() any { return svc.Stats() },
|
|
} {
|
|
handler := handleVerb(verb, source)
|
|
if err := svc.addVerbHandlers(nc, verb, handler); err != nil {
|
|
svc.asyncDispatcher.close()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
svc.started = time.Now().UTC()
|
|
return svc, nil
|
|
}
|
|
|
|
func (s *service) AddEndpoint(name string, handler Handler, opts ...EndpointOpt) error {
|
|
var options endpointOpts
|
|
for _, opt := range opts {
|
|
if err := opt(&options); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
subject := name
|
|
if options.subject != "" {
|
|
subject = options.subject
|
|
}
|
|
queueGroup, noQueue := resolveQueueGroup(options.queueGroup, s.Config.QueueGroup, options.qgDisabled, s.Config.QueueGroupDisabled)
|
|
return addEndpoint(s, name, subject, handler, options.metadata, queueGroup, noQueue, options.msgLimit, options.bytesLimit)
|
|
}
|
|
|
|
func addEndpoint(s *service, name, subject string, handler Handler, metadata map[string]string, queueGroup string, noQueue bool, msgLimit, bytesLimit int) error {
|
|
if !nameRegexp.MatchString(name) {
|
|
return fmt.Errorf("%w: invalid endpoint name", ErrConfigValidation)
|
|
}
|
|
if !subjectRegexp.MatchString(subject) {
|
|
return fmt.Errorf("%w: invalid endpoint subject", ErrConfigValidation)
|
|
}
|
|
if !subjectRegexp.MatchString(queueGroup) {
|
|
return fmt.Errorf("%w: invalid endpoint queue group", ErrConfigValidation)
|
|
}
|
|
endpoint := &Endpoint{
|
|
service: s,
|
|
EndpointConfig: EndpointConfig{
|
|
Subject: subject,
|
|
Handler: handler,
|
|
Metadata: metadata,
|
|
QueueGroup: queueGroup,
|
|
QueueGroupDisabled: noQueue,
|
|
},
|
|
Name: name,
|
|
}
|
|
|
|
var sub *nats.Subscription
|
|
var err error
|
|
var options = endpointOpts{
|
|
msgLimit: msgLimit,
|
|
bytesLimit: bytesLimit,
|
|
}
|
|
|
|
if !noQueue {
|
|
sub, err = s.nc.QueueSubscribe(
|
|
subject,
|
|
queueGroup,
|
|
func(m *nats.Msg) {
|
|
s.reqHandler(endpoint, &request{msg: m})
|
|
},
|
|
)
|
|
} else {
|
|
sub, err = s.nc.Subscribe(
|
|
subject,
|
|
func(m *nats.Msg) {
|
|
s.reqHandler(endpoint, &request{msg: m})
|
|
},
|
|
)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Apply pending limits if configured
|
|
if options.msgLimit != 0 || options.bytesLimit != 0 {
|
|
if err := sub.SetPendingLimits(options.msgLimit, options.bytesLimit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
s.m.Lock()
|
|
endpoint.subscription = sub
|
|
s.endpoints = append(s.endpoints, endpoint)
|
|
endpoint.stats = EndpointStats{
|
|
Name: name,
|
|
Subject: subject,
|
|
QueueGroup: queueGroup,
|
|
}
|
|
s.m.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *service) AddGroup(name string, opts ...GroupOpt) Group {
|
|
var o groupOpts
|
|
for _, opt := range opts {
|
|
opt(&o)
|
|
}
|
|
queueGroup, noQueue := resolveQueueGroup(o.queueGroup, s.Config.QueueGroup, o.qgDisabled, s.Config.QueueGroupDisabled)
|
|
return &group{
|
|
service: s,
|
|
prefix: name,
|
|
queueGroup: queueGroup,
|
|
queueGroupDisabled: noQueue,
|
|
}
|
|
}
|
|
|
|
// dispatch is responsible for calling any async callbacks
|
|
func (ac *asyncCallbacksHandler) run() {
|
|
for {
|
|
f, ok := <-ac.cbQueue
|
|
if !ok || 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() {
|
|
if ac.closed {
|
|
return
|
|
}
|
|
close(ac.cbQueue)
|
|
ac.closed = true
|
|
}
|
|
|
|
func (c *Config) valid() error {
|
|
if !nameRegexp.MatchString(c.Name) {
|
|
return fmt.Errorf("%w: service name: name should not be empty and should consist of alphanumerical characters, dashes and underscores", ErrConfigValidation)
|
|
}
|
|
if !semVerRegexp.MatchString(c.Version) {
|
|
return fmt.Errorf("%w: version: version should not be empty should match the SemVer format", ErrConfigValidation)
|
|
}
|
|
if c.QueueGroup != "" && !subjectRegexp.MatchString(c.QueueGroup) {
|
|
return fmt.Errorf("%w: queue group: invalid queue group name", ErrConfigValidation)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) wrapConnectionEventCallbacks() {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
s.natsHandlers.closed = s.nc.ClosedHandler()
|
|
if s.natsHandlers.closed != nil {
|
|
s.nc.SetClosedHandler(func(c *nats.Conn) {
|
|
s.Stop()
|
|
s.natsHandlers.closed(c)
|
|
})
|
|
} else {
|
|
s.nc.SetClosedHandler(func(c *nats.Conn) {
|
|
s.Stop()
|
|
})
|
|
}
|
|
|
|
s.natsHandlers.asyncErr = s.nc.ErrorHandler()
|
|
if s.natsHandlers.asyncErr != nil {
|
|
s.nc.SetErrorHandler(func(c *nats.Conn, sub *nats.Subscription, err error) {
|
|
if sub == nil {
|
|
s.natsHandlers.asyncErr(c, sub, err)
|
|
return
|
|
}
|
|
endpoint, match := s.matchSubscriptionSubject(sub.Subject)
|
|
if !match {
|
|
s.natsHandlers.asyncErr(c, sub, err)
|
|
return
|
|
}
|
|
if s.Config.ErrorHandler != nil {
|
|
s.Config.ErrorHandler(s, &NATSError{
|
|
Subject: sub.Subject,
|
|
Description: err.Error(),
|
|
err: err,
|
|
})
|
|
}
|
|
s.m.Lock()
|
|
if endpoint != nil {
|
|
endpoint.stats.NumErrors++
|
|
endpoint.stats.LastError = err.Error()
|
|
}
|
|
s.m.Unlock()
|
|
if stopErr := s.Stop(); stopErr != nil {
|
|
s.natsHandlers.asyncErr(c, sub, errors.Join(err, fmt.Errorf("stopping service: %w", stopErr)))
|
|
} else {
|
|
s.natsHandlers.asyncErr(c, sub, err)
|
|
}
|
|
})
|
|
} else {
|
|
s.nc.SetErrorHandler(func(c *nats.Conn, sub *nats.Subscription, err error) {
|
|
if sub == nil {
|
|
return
|
|
}
|
|
endpoint, match := s.matchSubscriptionSubject(sub.Subject)
|
|
if !match {
|
|
return
|
|
}
|
|
if s.Config.ErrorHandler != nil {
|
|
s.Config.ErrorHandler(s, &NATSError{
|
|
Subject: sub.Subject,
|
|
Description: err.Error(),
|
|
err: err,
|
|
})
|
|
}
|
|
s.m.Lock()
|
|
if endpoint != nil {
|
|
endpoint.stats.NumErrors++
|
|
endpoint.stats.LastError = err.Error()
|
|
}
|
|
s.m.Unlock()
|
|
s.Stop()
|
|
})
|
|
}
|
|
}
|
|
|
|
func unwrapConnectionEventCallbacks(nc *nats.Conn, handlers handlers) {
|
|
if nc.IsClosed() {
|
|
return
|
|
}
|
|
nc.SetClosedHandler(handlers.closed)
|
|
nc.SetErrorHandler(handlers.asyncErr)
|
|
}
|
|
|
|
func (s *service) matchSubscriptionSubject(subj string) (*Endpoint, bool) {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
for _, verbSub := range s.verbSubs {
|
|
if verbSub.Subject == subj {
|
|
return nil, true
|
|
}
|
|
}
|
|
for _, e := range s.endpoints {
|
|
if matchEndpointSubject(e.Subject, subj) {
|
|
return e, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func matchEndpointSubject(endpointSubject, literalSubject string) bool {
|
|
subjectTokens := strings.Split(literalSubject, ".")
|
|
endpointTokens := strings.Split(endpointSubject, ".")
|
|
if len(endpointTokens) > len(subjectTokens) {
|
|
return false
|
|
}
|
|
for i, et := range endpointTokens {
|
|
if i == len(endpointTokens)-1 && et == ">" {
|
|
return true
|
|
}
|
|
if et != subjectTokens[i] && et != "*" {
|
|
return false
|
|
}
|
|
}
|
|
// Without a trailing ">", every subject token must be consumed; otherwise a
|
|
// shorter endpoint would over-match a longer subject (e.g. "foo" vs
|
|
// "foo.bar").
|
|
return len(endpointTokens) == len(subjectTokens)
|
|
}
|
|
|
|
// addVerbHandlers 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) addVerbHandlers(nc *nats.Conn, verb Verb, handler HandlerFunc) 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 HandlerFunc) error {
|
|
subj, err := ControlSubject(verb, kind, id)
|
|
if err != nil {
|
|
if stopErr := s.Stop(); stopErr != nil {
|
|
return errors.Join(err, fmt.Errorf("stopping service: %w", stopErr))
|
|
}
|
|
return err
|
|
}
|
|
|
|
s.verbSubs[name], err = nc.Subscribe(subj, func(msg *nats.Msg) {
|
|
handler(&request{msg: msg})
|
|
})
|
|
if err != nil {
|
|
if stopErr := s.Stop(); stopErr != nil {
|
|
return errors.Join(err, fmt.Errorf("stopping service: %w", stopErr))
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// reqHandler invokes the service request handler and modifies service stats
|
|
func (s *service) reqHandler(endpoint *Endpoint, req *request) {
|
|
start := time.Now()
|
|
endpoint.Handler.Handle(req)
|
|
s.m.Lock()
|
|
endpoint.stats.NumRequests++
|
|
endpoint.stats.ProcessingTime += time.Since(start)
|
|
avgProcessingTime := endpoint.stats.ProcessingTime.Nanoseconds() / int64(endpoint.stats.NumRequests)
|
|
endpoint.stats.AverageProcessingTime = time.Duration(avgProcessingTime)
|
|
|
|
if req.respondError != nil {
|
|
endpoint.stats.NumErrors++
|
|
endpoint.stats.LastError = req.respondError.Error()
|
|
}
|
|
s.m.Unlock()
|
|
}
|
|
|
|
// Stop drains the endpoint subscriptions and marks the service as stopped.
|
|
func (s *service) Stop() error {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
if s.stopped {
|
|
return nil
|
|
}
|
|
// make a copy of s.endpoints to range over in order to stop
|
|
// since *Endpoint.stop manipulates s.endpoints!
|
|
endpointsToStop := append(make([]*Endpoint, 0, len(s.endpoints)), s.endpoints...)
|
|
for _, e := range endpointsToStop {
|
|
if err := e.stop(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var keys []string
|
|
for key, sub := range s.verbSubs {
|
|
keys = append(keys, key)
|
|
if err := sub.Drain(); err != nil {
|
|
// connection is closed so draining is not possible
|
|
if errors.Is(err, nats.ErrConnectionClosed) {
|
|
break
|
|
}
|
|
return fmt.Errorf("draining subscription for subject %q: %w", sub.Subject, err)
|
|
}
|
|
}
|
|
for _, key := range keys {
|
|
delete(s.verbSubs, key)
|
|
}
|
|
unwrapConnectionEventCallbacks(s.nc, s.natsHandlers)
|
|
s.stopped = true
|
|
if s.DoneHandler != nil {
|
|
s.asyncDispatcher.push(func() { s.DoneHandler(s) })
|
|
}
|
|
s.asyncDispatcher.close()
|
|
return nil
|
|
}
|
|
|
|
func (s *service) serviceIdentity() ServiceIdentity {
|
|
return ServiceIdentity{
|
|
Name: s.Config.Name,
|
|
ID: s.id,
|
|
Version: s.Config.Version,
|
|
Metadata: s.Config.Metadata,
|
|
}
|
|
}
|
|
|
|
// Info returns information about the service
|
|
func (s *service) Info() Info {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
|
|
endpoints := make([]EndpointInfo, 0, len(s.endpoints))
|
|
for _, e := range s.endpoints {
|
|
endpoints = append(endpoints, EndpointInfo{
|
|
Name: e.Name,
|
|
Subject: e.Subject,
|
|
QueueGroup: e.QueueGroup,
|
|
Metadata: e.Metadata,
|
|
})
|
|
}
|
|
|
|
return Info{
|
|
ServiceIdentity: s.serviceIdentity(),
|
|
Type: InfoResponseType,
|
|
Description: s.Config.Description,
|
|
Endpoints: endpoints,
|
|
}
|
|
}
|
|
|
|
// Stats returns statistics for the service endpoint and all monitoring endpoints.
|
|
func (s *service) Stats() Stats {
|
|
s.m.Lock()
|
|
defer s.m.Unlock()
|
|
|
|
stats := Stats{
|
|
ServiceIdentity: s.serviceIdentity(),
|
|
Endpoints: make([]*EndpointStats, 0),
|
|
Type: StatsResponseType,
|
|
Started: s.started,
|
|
}
|
|
for _, endpoint := range s.endpoints {
|
|
endpointStats := &EndpointStats{
|
|
Name: endpoint.stats.Name,
|
|
Subject: endpoint.stats.Subject,
|
|
QueueGroup: endpoint.stats.QueueGroup,
|
|
NumRequests: endpoint.stats.NumRequests,
|
|
NumErrors: endpoint.stats.NumErrors,
|
|
LastError: endpoint.stats.LastError,
|
|
ProcessingTime: endpoint.stats.ProcessingTime,
|
|
AverageProcessingTime: endpoint.stats.AverageProcessingTime,
|
|
}
|
|
if s.StatsHandler != nil {
|
|
data, _ := json.Marshal(s.StatsHandler(endpoint))
|
|
endpointStats.Data = data
|
|
}
|
|
stats.Endpoints = append(stats.Endpoints, endpointStats)
|
|
}
|
|
return stats
|
|
}
|
|
|
|
// Reset resets all statistics on a service instance.
|
|
func (s *service) Reset() {
|
|
s.m.Lock()
|
|
for _, endpoint := range s.endpoints {
|
|
endpoint.reset()
|
|
}
|
|
s.started = time.Now().UTC()
|
|
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
|
|
}
|
|
|
|
func (e *NATSError) Error() string {
|
|
return fmt.Sprintf("%q: %s", e.Subject, e.Description)
|
|
}
|
|
|
|
// Unwrap returns the underlying error if any.
|
|
func (e *NATSError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
// Is reports whether the target error is equal to this error.
|
|
func (e *NATSError) Is(target error) bool {
|
|
if e == nil {
|
|
return false
|
|
}
|
|
if t, ok := target.(*NATSError); ok {
|
|
return e.Subject == t.Subject && e.Description == t.Description
|
|
}
|
|
return e.err != nil && errors.Is(e.err, target)
|
|
}
|
|
|
|
func (g *group) AddEndpoint(name string, handler Handler, opts ...EndpointOpt) error {
|
|
var options endpointOpts
|
|
for _, opt := range opts {
|
|
if err := opt(&options); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
subject := name
|
|
if options.subject != "" {
|
|
subject = options.subject
|
|
}
|
|
endpointSubject := fmt.Sprintf("%s.%s", g.prefix, subject)
|
|
if g.prefix == "" {
|
|
endpointSubject = subject
|
|
}
|
|
queueGroup, noQueue := resolveQueueGroup(options.queueGroup, g.queueGroup, options.qgDisabled, g.queueGroupDisabled)
|
|
|
|
return addEndpoint(g.service, name, endpointSubject, handler, options.metadata, queueGroup, noQueue, options.msgLimit, options.bytesLimit)
|
|
}
|
|
|
|
func resolveQueueGroup(customQG, parentQG string, disabled, parentDisabled bool) (string, bool) {
|
|
if disabled {
|
|
return "", true
|
|
}
|
|
if customQG != "" {
|
|
return customQG, false
|
|
}
|
|
if parentDisabled {
|
|
return "", true
|
|
}
|
|
if parentQG != "" {
|
|
return parentQG, false
|
|
}
|
|
return DefaultQueueGroup, false
|
|
}
|
|
|
|
func (g *group) AddGroup(name string, opts ...GroupOpt) Group {
|
|
var o groupOpts
|
|
for _, opt := range opts {
|
|
opt(&o)
|
|
}
|
|
queueGroup, noQueue := resolveQueueGroup(o.queueGroup, g.queueGroup, o.qgDisabled, g.queueGroupDisabled)
|
|
|
|
parts := make([]string, 0, 2)
|
|
if g.prefix != "" {
|
|
parts = append(parts, g.prefix)
|
|
}
|
|
if name != "" {
|
|
parts = append(parts, name)
|
|
}
|
|
prefix := strings.Join(parts, ".")
|
|
|
|
return &group{
|
|
service: g.service,
|
|
prefix: prefix,
|
|
queueGroup: queueGroup,
|
|
queueGroupDisabled: noQueue,
|
|
}
|
|
}
|
|
|
|
func (e *Endpoint) stop() error {
|
|
// Drain the subscription. If the connection is closed, draining is not possible
|
|
// but we should still remove the endpoint from the service.
|
|
if err := e.subscription.Drain(); err != nil && !errors.Is(err, nats.ErrConnectionClosed) {
|
|
return fmt.Errorf("draining subscription for request handler: %w", err)
|
|
}
|
|
for i := 0; i < len(e.service.endpoints); i++ {
|
|
if e.service.endpoints[i].Subject == e.Subject {
|
|
if i != len(e.service.endpoints)-1 {
|
|
e.service.endpoints = append(e.service.endpoints[:i], e.service.endpoints[i+1:]...)
|
|
} else {
|
|
e.service.endpoints = e.service.endpoints[:i]
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Endpoint) reset() {
|
|
e.stats = EndpointStats{
|
|
Name: e.stats.Name,
|
|
Subject: e.stats.Subject,
|
|
}
|
|
}
|
|
|
|
// ControlSubject returns monitoring subjects used by the Service.
|
|
// Providing a verb is mandatory (it should be one of Ping, 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
|
|
}
|
|
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 WithEndpointSubject(subject string) EndpointOpt {
|
|
return func(e *endpointOpts) error {
|
|
e.subject = subject
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithEndpointMetadata(metadata map[string]string) EndpointOpt {
|
|
return func(e *endpointOpts) error {
|
|
e.metadata = metadata
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithEndpointMetadataKey adds a key-value pair to the endpoints's metadata.
|
|
// Prefer using WithEndpointMetadata when you have all the key-value pairs you
|
|
// want to add at once or when you want to replace any existing metadata.
|
|
func WithEndpointMetadataKey(key, value string) EndpointOpt {
|
|
return func(e *endpointOpts) error {
|
|
if e.metadata == nil {
|
|
e.metadata = map[string]string{}
|
|
}
|
|
e.metadata[key] = value
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithEndpointQueueGroup(queueGroup string) EndpointOpt {
|
|
return func(e *endpointOpts) error {
|
|
e.queueGroup = queueGroup
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithEndpointQueueGroupDisabled() EndpointOpt {
|
|
return func(e *endpointOpts) error {
|
|
e.qgDisabled = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithEndpointPendingLimits sets the pending limits for the endpoint's
|
|
// subscription. These limits how many messages and/or bytes can be buffered in
|
|
// memory before the subscription is terminated with nats.ErrSlowConsumer.
|
|
// Either limit can be set to -1 to indicate no limit.
|
|
func WithEndpointPendingLimits(msgLimit, bytesLimit int) EndpointOpt {
|
|
return func(e *endpointOpts) error {
|
|
if msgLimit == 0 && bytesLimit == 0 {
|
|
return fmt.Errorf("%w: at least one pending limit must be non-zero", ErrConfigValidation)
|
|
}
|
|
e.msgLimit = msgLimit
|
|
e.bytesLimit = bytesLimit
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithGroupQueueGroup(queueGroup string) GroupOpt {
|
|
return func(g *groupOpts) {
|
|
g.queueGroup = queueGroup
|
|
}
|
|
}
|
|
|
|
func WithGroupQueueGroupDisabled() GroupOpt {
|
|
return func(g *groupOpts) {
|
|
g.qgDisabled = true
|
|
}
|
|
}
|