github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/events/subscription_manager.go (about)

     1  // Copyright © 2021 Kaleido, Inc.
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  //
     5  // Licensed under the Apache License, Version 2.0 (the "License");
     6  // you may not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing, software
    12  // distributed under the License is distributed on an "AS IS" BASIS,
    13  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  // See the License for the specific language governing permissions and
    15  // limitations under the License.
    16  
    17  package events
    18  
    19  import (
    20  	"context"
    21  	"regexp"
    22  	"sync"
    23  
    24  	"github.com/kaleido-io/firefly/internal/config"
    25  	"github.com/kaleido-io/firefly/internal/events/eifactory"
    26  	"github.com/kaleido-io/firefly/internal/i18n"
    27  	"github.com/kaleido-io/firefly/internal/log"
    28  	"github.com/kaleido-io/firefly/internal/retry"
    29  	"github.com/kaleido-io/firefly/pkg/database"
    30  	"github.com/kaleido-io/firefly/pkg/events"
    31  	"github.com/kaleido-io/firefly/pkg/fftypes"
    32  )
    33  
    34  type subscription struct {
    35  	definition *fftypes.Subscription
    36  
    37  	dispatcherElection chan bool
    38  	eventMatcher       *regexp.Regexp
    39  	groupFilter        *regexp.Regexp
    40  	tagFilter          *regexp.Regexp
    41  	topicsFilter       *regexp.Regexp
    42  }
    43  
    44  type connection struct {
    45  	id          string
    46  	matcher     events.SubscriptionMatcher
    47  	dispatchers map[fftypes.UUID]*eventDispatcher
    48  	ei          events.Plugin
    49  }
    50  
    51  type subscriptionManager struct {
    52  	ctx                  context.Context
    53  	database             database.Plugin
    54  	eventNotifier        *eventNotifier
    55  	transports           map[string]events.Plugin
    56  	connections          map[string]*connection
    57  	mux                  sync.Mutex
    58  	maxSubs              uint64
    59  	durableSubs          map[fftypes.UUID]*subscription
    60  	cancelCtx            func()
    61  	newSubscriptions     chan *fftypes.UUID
    62  	deletedSubscriptions chan *fftypes.UUID
    63  	retry                retry.Retry
    64  }
    65  
    66  func newSubscriptionManager(ctx context.Context, di database.Plugin, en *eventNotifier) (*subscriptionManager, error) {
    67  	ctx, cancelCtx := context.WithCancel(ctx)
    68  	sm := &subscriptionManager{
    69  		ctx:                  ctx,
    70  		database:             di,
    71  		transports:           make(map[string]events.Plugin),
    72  		connections:          make(map[string]*connection),
    73  		durableSubs:          make(map[fftypes.UUID]*subscription),
    74  		newSubscriptions:     make(chan *fftypes.UUID),
    75  		deletedSubscriptions: make(chan *fftypes.UUID),
    76  		maxSubs:              uint64(config.GetUint(config.SubscriptionMax)),
    77  		cancelCtx:            cancelCtx,
    78  		eventNotifier:        en,
    79  		retry: retry.Retry{
    80  			InitialDelay: config.GetDuration(config.SubscriptionsRetryInitialDelay),
    81  			MaximumDelay: config.GetDuration(config.SubscriptionsRetryMaxDelay),
    82  			Factor:       config.GetFloat64(config.SubscriptionsRetryFactor),
    83  		},
    84  	}
    85  
    86  	err := sm.loadTransports()
    87  	if err == nil {
    88  		err = sm.initTransports()
    89  	}
    90  	return sm, err
    91  }
    92  
    93  func (sm *subscriptionManager) loadTransports() error {
    94  	var err error
    95  	enabledTransports := config.GetStringSlice(config.EventTransportsEnabled)
    96  	for _, transport := range enabledTransports {
    97  		sm.transports[transport], err = eifactory.GetPlugin(sm.ctx, transport)
    98  		if err != nil {
    99  			return err
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  func (sm *subscriptionManager) initTransports() error {
   106  	var err error
   107  	for _, ei := range sm.transports {
   108  		prefix := config.NewPluginConfig("events").SubPrefix(ei.Name())
   109  		ei.InitPrefix(prefix)
   110  		err = ei.Init(sm.ctx, prefix, &boundCallbacks{sm: sm, ei: ei})
   111  		if err != nil {
   112  			return err
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  func (sm *subscriptionManager) start() error {
   119  	fb := database.SubscriptionQueryFactory.NewFilter(sm.ctx)
   120  	filter := fb.And().Limit(sm.maxSubs)
   121  	persistedSubs, err := sm.database.GetSubscriptions(sm.ctx, filter)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	sm.mux.Lock()
   126  	defer sm.mux.Unlock()
   127  	for _, subDef := range persistedSubs {
   128  		newSub, err := sm.parseSubscriptionDef(sm.ctx, subDef)
   129  		if err != nil {
   130  			// Warn and continue startup
   131  			log.L(sm.ctx).Warnf("Failed to reload subscription %s:%s [%s]: %s", subDef.Namespace, subDef.Name, subDef.ID, err)
   132  			continue
   133  		}
   134  		sm.durableSubs[*subDef.ID] = newSub
   135  	}
   136  	go sm.subscriptionEventListener()
   137  	return nil
   138  }
   139  
   140  func (sm *subscriptionManager) subscriptionEventListener() {
   141  	for {
   142  		select {
   143  		case id := <-sm.newSubscriptions:
   144  			go sm.newDurableSubscription(id)
   145  		case id := <-sm.deletedSubscriptions:
   146  			go sm.deletedDurableSubscription(id)
   147  		case <-sm.ctx.Done():
   148  			return
   149  		}
   150  	}
   151  }
   152  
   153  func (sm *subscriptionManager) newDurableSubscription(id *fftypes.UUID) {
   154  	var subDef *fftypes.Subscription
   155  	err := sm.retry.Do(sm.ctx, "retrieve subscription", func(attempt int) (retry bool, err error) {
   156  		subDef, err = sm.database.GetSubscriptionByID(sm.ctx, id)
   157  		return err != nil, err // indefinite retry
   158  	})
   159  	if err != nil || subDef == nil {
   160  		// either the context was cancelled (so we're closing), or the subscription no longer exists
   161  		log.L(sm.ctx).Infof("Unable to process new subscription event for id=%s (%v)", id, err)
   162  		return
   163  	}
   164  
   165  	log.L(sm.ctx).Infof("Created subscription %s:%s [%s]", subDef.Namespace, subDef.Name, subDef.ID)
   166  
   167  	newSub, err := sm.parseSubscriptionDef(sm.ctx, subDef)
   168  	if err != nil {
   169  		// Swallow this, as the subscription is simply invalid
   170  		log.L(sm.ctx).Errorf("Subscription rejected by subscription manager: %s", err)
   171  		return
   172  	}
   173  
   174  	// Now we're ready to update our locked state, adding this subscription to our
   175  	// in-memory table, and creating any missing dispatchers
   176  	sm.mux.Lock()
   177  	defer sm.mux.Unlock()
   178  	if sm.durableSubs[*subDef.ID] == nil {
   179  		sm.durableSubs[*subDef.ID] = newSub
   180  		for _, conn := range sm.connections {
   181  			if conn.matcher != nil && conn.matcher(subDef.SubscriptionRef) {
   182  				sm.matchedSubscriptionWithLock(conn, newSub)
   183  			}
   184  		}
   185  	}
   186  }
   187  
   188  func (sm *subscriptionManager) deletedDurableSubscription(id *fftypes.UUID) {
   189  	var subDef *fftypes.Subscription
   190  	err := sm.retry.Do(sm.ctx, "retrieve subscription", func(attempt int) (retry bool, err error) {
   191  		subDef, err = sm.database.GetSubscriptionByID(sm.ctx, id)
   192  		return err != nil, err // indefinite retry
   193  	})
   194  	if err != nil || subDef == nil {
   195  		// either the context was cancelled (so we're closing), or the subscription no longer exists
   196  		log.L(sm.ctx).Infof("Unable to process deleted subscription event for id=%s (%v)", id, err)
   197  		return
   198  	}
   199  
   200  	sm.mux.Lock()
   201  	var dispatchers []*eventDispatcher
   202  	// Remove it from the list of durable subs (if there)
   203  	_, loaded := sm.durableSubs[*id]
   204  	if loaded {
   205  		delete(sm.durableSubs, *id)
   206  		// Find any active dispatchers, while we're in the lock, and remove them
   207  		for _, conn := range sm.connections {
   208  			dispatcher, ok := conn.dispatchers[*id]
   209  			if ok {
   210  				dispatchers = append(dispatchers, dispatcher)
   211  				delete(conn.dispatchers, *id)
   212  			}
   213  		}
   214  	}
   215  	sm.mux.Unlock()
   216  
   217  	log.L(sm.ctx).Infof("Deleting subscription %s:%s [%s] loaded=%t dispatchers=%d", subDef.Namespace, subDef.Name, subDef.ID, loaded, len(dispatchers))
   218  
   219  	// Outside the lock, close out the active dispatchers
   220  	for _, dispatcher := range dispatchers {
   221  		dispatcher.close()
   222  	}
   223  }
   224  
   225  func (sm *subscriptionManager) parseSubscriptionDef(ctx context.Context, subDef *fftypes.Subscription) (sub *subscription, err error) {
   226  	filter := subDef.Filter
   227  
   228  	if _, ok := sm.transports[subDef.Transport]; !ok {
   229  		return nil, i18n.NewError(ctx, i18n.MsgUnknownEventTransportPlugin, subDef.Transport)
   230  	}
   231  
   232  	var eventFilter *regexp.Regexp
   233  	if filter.Events != "" {
   234  		eventFilter, err = regexp.Compile(filter.Events)
   235  		if err != nil {
   236  			return nil, i18n.WrapError(ctx, err, i18n.MsgRegexpCompileFailed, "filter.events", filter.Events)
   237  		}
   238  	}
   239  
   240  	var tagFilter *regexp.Regexp
   241  	if filter.Tag != "" {
   242  		tagFilter, err = regexp.Compile(filter.Tag)
   243  		if err != nil {
   244  			return nil, i18n.WrapError(ctx, err, i18n.MsgRegexpCompileFailed, "filter.tag", filter.Tag)
   245  		}
   246  	}
   247  
   248  	var groupFilter *regexp.Regexp
   249  	if filter.Group != "" {
   250  		groupFilter, err = regexp.Compile(filter.Group)
   251  		if err != nil {
   252  			return nil, i18n.WrapError(ctx, err, i18n.MsgRegexpCompileFailed, "filter.group", filter.Group)
   253  		}
   254  	}
   255  
   256  	var topicsFilter *regexp.Regexp
   257  	if filter.Topics != "" {
   258  		topicsFilter, err = regexp.Compile(filter.Topics)
   259  		if err != nil {
   260  			return nil, i18n.WrapError(ctx, err, i18n.MsgRegexpCompileFailed, "filter.topics", filter.Topics)
   261  		}
   262  	}
   263  
   264  	sub = &subscription{
   265  		dispatcherElection: make(chan bool, 1),
   266  		definition:         subDef,
   267  		eventMatcher:       eventFilter,
   268  		groupFilter:        groupFilter,
   269  		tagFilter:          tagFilter,
   270  		topicsFilter:       topicsFilter,
   271  	}
   272  	return sub, err
   273  }
   274  
   275  func (sm *subscriptionManager) close() {
   276  	sm.mux.Lock()
   277  	conns := make([]*connection, 0, len(sm.connections))
   278  	for _, conn := range sm.connections {
   279  		conns = append(conns, conn)
   280  	}
   281  	sm.mux.Unlock()
   282  	for _, conn := range conns {
   283  		sm.connnectionClosed(conn.ei, conn.id)
   284  	}
   285  }
   286  
   287  func (sm *subscriptionManager) getCreateConnLocked(ei events.Plugin, connID string) *connection {
   288  	conn, ok := sm.connections[connID]
   289  	if !ok {
   290  		conn = &connection{
   291  			id:          connID,
   292  			dispatchers: make(map[fftypes.UUID]*eventDispatcher),
   293  			ei:          ei,
   294  		}
   295  		sm.connections[connID] = conn
   296  	}
   297  	return conn
   298  }
   299  
   300  func (sm *subscriptionManager) registerConnection(ei events.Plugin, connID string, matcher events.SubscriptionMatcher) error {
   301  	sm.mux.Lock()
   302  	defer sm.mux.Unlock()
   303  
   304  	// Check if there are existing dispatchers
   305  	conn := sm.getCreateConnLocked(ei, connID)
   306  	if conn.ei != ei {
   307  		return i18n.NewError(sm.ctx, i18n.MsgMismatchedTransport, connID, ei.Name(), conn.ei.Name())
   308  	}
   309  
   310  	// Update the matcher for this connection ID
   311  	conn.matcher = matcher
   312  
   313  	// Make sure we don't have dispatchers now for any that don't match
   314  	for subID, d := range conn.dispatchers {
   315  		if !d.subscription.definition.Ephemeral && !conn.matcher(d.subscription.definition.SubscriptionRef) {
   316  			d.close()
   317  			delete(conn.dispatchers, subID)
   318  		}
   319  	}
   320  	// Make new dispatchers for all durable subscriptions that match
   321  	for _, sub := range sm.durableSubs {
   322  		if conn.matcher(sub.definition.SubscriptionRef) {
   323  			sm.matchedSubscriptionWithLock(conn, sub)
   324  		}
   325  	}
   326  
   327  	return nil
   328  }
   329  
   330  func (sm *subscriptionManager) matchedSubscriptionWithLock(conn *connection, sub *subscription) {
   331  	ei, foundTransport := sm.transports[sub.definition.Transport]
   332  	if foundTransport {
   333  		if _, ok := conn.dispatchers[*sub.definition.ID]; !ok {
   334  			dispatcher := newEventDispatcher(sm.ctx, ei, sm.database, conn.id, sub, sm.eventNotifier)
   335  			conn.dispatchers[*sub.definition.ID] = dispatcher
   336  			dispatcher.start()
   337  		}
   338  	} else {
   339  		log.L(sm.ctx).Warnf("Subscription %s:%s [%s] defined for unknown transport '%s", sub.definition.Namespace, sub.definition.Name, sub.definition.ID, sub.definition.Transport)
   340  	}
   341  }
   342  
   343  func (sm *subscriptionManager) ephemeralSubscription(ei events.Plugin, connID, namespace string, filter fftypes.SubscriptionFilter, options fftypes.SubscriptionOptions) error {
   344  	sm.mux.Lock()
   345  	defer sm.mux.Unlock()
   346  
   347  	conn := sm.getCreateConnLocked(ei, connID)
   348  
   349  	if conn.ei != ei {
   350  		return i18n.NewError(sm.ctx, i18n.MsgMismatchedTransport, connID, ei.Name(), conn.ei.Name())
   351  	}
   352  
   353  	subID := fftypes.NewUUID()
   354  	subDefinition := &fftypes.Subscription{
   355  		SubscriptionRef: fftypes.SubscriptionRef{
   356  			ID:        subID,
   357  			Name:      subID.String(),
   358  			Namespace: namespace,
   359  		},
   360  		Transport: ei.Name(),
   361  		Ephemeral: true,
   362  		Filter:    filter,
   363  		Options:   options,
   364  		Created:   fftypes.Now(),
   365  	}
   366  
   367  	newSub, err := sm.parseSubscriptionDef(sm.ctx, subDefinition)
   368  	if err != nil {
   369  		return err
   370  	}
   371  
   372  	// Create the dispatcher, and start immediately
   373  	dispatcher := newEventDispatcher(sm.ctx, ei, sm.database, connID, newSub, sm.eventNotifier)
   374  	dispatcher.start()
   375  
   376  	conn.dispatchers[*subID] = dispatcher
   377  	return nil
   378  }
   379  
   380  func (sm *subscriptionManager) connnectionClosed(ei events.Plugin, connID string) {
   381  	sm.mux.Lock()
   382  	conn, ok := sm.connections[connID]
   383  	if ok && conn.ei != ei {
   384  		log.L(sm.ctx).Warnf(i18n.ExpandWithCode(sm.ctx, i18n.MsgMismatchedTransport, connID, ei.Name(), conn.ei.Name()))
   385  		sm.mux.Unlock()
   386  		return
   387  	}
   388  	delete(sm.connections, connID)
   389  	sm.mux.Unlock()
   390  
   391  	if !ok {
   392  		log.L(sm.ctx).Debugf("Connections already disposed: %s", connID)
   393  		return
   394  	}
   395  	log.L(sm.ctx).Debugf("Closing %d dispatcher(s) for connection '%s'", len(conn.dispatchers), connID)
   396  	for _, d := range conn.dispatchers {
   397  		d.close()
   398  	}
   399  }
   400  
   401  func (sm *subscriptionManager) deliveryResponse(ei events.Plugin, connID string, inflight fftypes.EventDeliveryResponse) error {
   402  	sm.mux.Lock()
   403  	var dispatcher *eventDispatcher
   404  	conn, ok := sm.connections[connID]
   405  	if ok && inflight.Subscription.ID != nil {
   406  		dispatcher = conn.dispatchers[*inflight.Subscription.ID]
   407  	}
   408  	sm.mux.Unlock()
   409  
   410  	if ok && conn.ei != ei {
   411  		return i18n.NewError(sm.ctx, i18n.MsgMismatchedTransport, connID, ei.Name(), conn.ei.Name())
   412  	}
   413  	if dispatcher == nil {
   414  		return i18n.NewError(sm.ctx, i18n.MsgConnSubscriptionNotStarted, inflight.Subscription.ID)
   415  	}
   416  
   417  	dispatcher.deliveryResponse(&inflight)
   418  	return nil
   419  }