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 }