initial commit
This commit is contained in:
2
internal/pubsub/doc.go
Normal file
2
internal/pubsub/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package pubsub provides functions and data for a NATS consumer listening for resource lifecycle events.
|
||||
package pubsub
|
||||
220
internal/pubsub/subscriber.go
Normal file
220
internal/pubsub/subscriber.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
nc "github.com/nats-io/nats.go"
|
||||
"go.infratographer.com/x/events"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.equinixmetal.net/infra9-metal-bridge/internal/service"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("go.infratographer.com/permissions-api/internal/pubsub")
|
||||
|
||||
// Subscriber is the subscriber client
|
||||
type Subscriber struct {
|
||||
ctx context.Context
|
||||
changeChannels []<-chan *message.Message
|
||||
logger *zap.SugaredLogger
|
||||
subscriber *events.Subscriber
|
||||
subOpts []nc.SubOpt
|
||||
svc service.Service
|
||||
}
|
||||
|
||||
// SubscriberOption is a functional option for the Subscriber
|
||||
type SubscriberOption func(s *Subscriber)
|
||||
|
||||
// WithLogger sets the logger for the Subscriber
|
||||
func WithLogger(l *zap.SugaredLogger) SubscriberOption {
|
||||
return func(s *Subscriber) {
|
||||
s.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// WithNatsSubOpts sets the logger for the Subscriber
|
||||
func WithNatsSubOpts(options ...nc.SubOpt) SubscriberOption {
|
||||
return func(s *Subscriber) {
|
||||
s.subOpts = append(s.subOpts, options...)
|
||||
}
|
||||
}
|
||||
|
||||
// NewSubscriber creates a new Subscriber
|
||||
func NewSubscriber(ctx context.Context, cfg events.SubscriberConfig, service service.Service, opts ...SubscriberOption) (*Subscriber, error) {
|
||||
s := &Subscriber{
|
||||
ctx: ctx,
|
||||
logger: zap.NewNop().Sugar(),
|
||||
svc: service,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
|
||||
sub, err := events.NewSubscriber(cfg, s.subOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.subscriber = sub
|
||||
|
||||
s.logger.Debugw("subscriber configuration", "config", cfg)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Subscribe subscribes to a nats subject
|
||||
func (s *Subscriber) Subscribe(topic string) error {
|
||||
msgChan, err := s.subscriber.SubscribeChanges(s.ctx, topic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.changeChannels = append(s.changeChannels, msgChan)
|
||||
|
||||
s.logger.Infof("Subscribing to topic %s", topic)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Listen start listening for messages on registered subjects and calls the registered message handler
|
||||
func (s Subscriber) Listen() error {
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
// goroutine for each change channel
|
||||
for _, ch := range s.changeChannels {
|
||||
wg.Add(1)
|
||||
|
||||
go s.listen(ch, wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listen listens for messages on a channel and calls the registered message handler
|
||||
func (s Subscriber) listen(messages <-chan *message.Message, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for msg := range messages {
|
||||
s.logger.Infow("processing event", "event.id", msg.UUID)
|
||||
if err := s.processEvent(msg); err != nil {
|
||||
s.logger.Warn("Failed to process msg: ", err)
|
||||
|
||||
s.logger.Infow("message nacked", "event.id", msg.UUID)
|
||||
msg.Nack()
|
||||
} else {
|
||||
s.logger.Infow("message acked", "event.id", msg.UUID)
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the subscriber connection and unsubscribes from all subscriptions
|
||||
func (s *Subscriber) Close() error {
|
||||
return s.subscriber.Close()
|
||||
}
|
||||
|
||||
// processEvent event message handler
|
||||
func (s *Subscriber) processEvent(msg *message.Message) error {
|
||||
changeMsg, err := events.UnmarshalChangeMessage(msg.Payload)
|
||||
if err != nil {
|
||||
s.logger.Errorw("failed to process data in msg", zap.Error(err))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(context.Background(), "pubsub.receive", trace.WithAttributes(attribute.String("pubsub.subject", changeMsg.SubjectID.String())))
|
||||
|
||||
defer span.End()
|
||||
|
||||
switch events.ChangeType(changeMsg.EventType) {
|
||||
case events.CreateChangeType:
|
||||
err = s.handleTouchEvent(ctx, msg, changeMsg)
|
||||
case events.UpdateChangeType:
|
||||
err = s.handleTouchEvent(ctx, msg, changeMsg)
|
||||
case events.DeleteChangeType:
|
||||
err = s.handleDeleteEvent(ctx, msg, changeMsg)
|
||||
default:
|
||||
s.logger.Warnw("ignoring msg, not a create, update or delete event", "event_type", changeMsg.EventType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Subscriber) handleTouchEvent(ctx context.Context, msg *message.Message, changeMsg events.ChangeMessage) error {
|
||||
if s.svc.IsOrganizationID(changeMsg.SubjectID) {
|
||||
if err := s.svc.TouchOrganization(ctx, changeMsg.SubjectID); err != nil {
|
||||
// TODO: only return errors on retryable errors
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.svc.IsProjectID(changeMsg.SubjectID) {
|
||||
if err := s.svc.TouchProject(ctx, changeMsg.SubjectID); err != nil {
|
||||
// TODO: only return errors on retryable errors
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.svc.IsUser(changeMsg.SubjectID) {
|
||||
if err := s.svc.TouchUser(ctx, changeMsg.SubjectID); err != nil {
|
||||
// TODO: only return errors on retryable errors
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Warnw("unknown subject id", "subject.id", changeMsg.SubjectID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Subscriber) handleDeleteEvent(ctx context.Context, msg *message.Message, changeMsg events.ChangeMessage) error {
|
||||
if s.svc.IsOrganizationID(changeMsg.SubjectID) {
|
||||
if err := s.svc.DeleteOrganization(ctx, changeMsg.SubjectID); err != nil {
|
||||
// TODO: only return errors on retryable errors
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.svc.IsProjectID(changeMsg.SubjectID) {
|
||||
if err := s.svc.DeleteProject(ctx, changeMsg.SubjectID); err != nil {
|
||||
// TODO: only return errors on retryable errors
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.svc.IsUser(changeMsg.SubjectID) {
|
||||
if err := s.svc.DeleteUser(ctx, changeMsg.SubjectID); err != nil {
|
||||
// TODO: only return errors on retryable errors
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Warnw("unknown subject id", "subject.id", changeMsg.SubjectID)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user