Files
mesh-controller/vendor/github.com/nats-io/nats.go/micro/service.go
T
jochen e1f5d4fdf0 Vendor every dependency, so no build fetches the host's validator (hq to-be 45 D1)
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.
2026-10-06 10:29:10 +02:00

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
}
}