2020-08-31 17:19:28 +00:00
|
|
|
package stream
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"sync/atomic"
|
2020-10-04 19:12:35 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2020-08-31 17:19:28 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// subscriptionStateOpen is the default state of a subscription. An open
|
|
|
|
// subscription may receive new events.
|
|
|
|
subscriptionStateOpen uint32 = 0
|
|
|
|
|
|
|
|
// subscriptionStateClosed indicates that the subscription was closed, possibly
|
|
|
|
// as a result of a change to an ACL token, and will not receive new events.
|
|
|
|
// The subscriber must issue a new Subscribe request.
|
|
|
|
subscriptionStateClosed uint32 = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
// ErrSubscriptionClosed is a error signalling the subscription has been
|
|
|
|
// closed. The client should Unsubscribe, then re-Subscribe.
|
|
|
|
var ErrSubscriptionClosed = errors.New("subscription closed by server, client should resubscribe")
|
|
|
|
|
|
|
|
type Subscription struct {
|
2020-10-08 18:27:52 +00:00
|
|
|
// state must be accessed atomically 0 means open, 1 means closed with reload
|
2020-08-31 17:19:28 +00:00
|
|
|
state uint32
|
|
|
|
|
|
|
|
req *SubscribeRequest
|
|
|
|
|
|
|
|
// currentItem stores the current buffer item we are on. It
|
|
|
|
// is mutated by calls to Next.
|
|
|
|
currentItem *bufferItem
|
|
|
|
|
|
|
|
// forceClosed is closed when forceClose is called. It is used by
|
2020-10-08 18:27:52 +00:00
|
|
|
// EventBroker to cancel Next().
|
2020-08-31 17:19:28 +00:00
|
|
|
forceClosed chan struct{}
|
|
|
|
|
2020-10-08 18:27:52 +00:00
|
|
|
// unsub is a function set by EventBroker that is called to free resources
|
2020-08-31 17:19:28 +00:00
|
|
|
// when the subscription is no longer needed.
|
|
|
|
// It must be safe to call the function from multiple goroutines and the function
|
|
|
|
// must be idempotent.
|
|
|
|
unsub func()
|
|
|
|
}
|
|
|
|
|
|
|
|
type SubscribeRequest struct {
|
2020-10-08 15:57:21 +00:00
|
|
|
Token string
|
|
|
|
Index uint64
|
|
|
|
Namespace string
|
2020-08-31 17:19:28 +00:00
|
|
|
|
2020-10-04 19:12:35 +00:00
|
|
|
Topics map[structs.Topic][]string
|
2020-10-08 18:27:52 +00:00
|
|
|
|
|
|
|
// StartExactlyAtIndex specifies if a subscription needs to
|
|
|
|
// start exactly at the requested Index. If set to false,
|
|
|
|
// the closest index in the buffer will be returned if there is not
|
|
|
|
// an exact match
|
|
|
|
StartExactlyAtIndex bool
|
2020-08-31 17:19:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func newSubscription(req *SubscribeRequest, item *bufferItem, unsub func()) *Subscription {
|
|
|
|
return &Subscription{
|
|
|
|
forceClosed: make(chan struct{}),
|
|
|
|
req: req,
|
|
|
|
currentItem: item,
|
|
|
|
unsub: unsub,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-04 19:12:35 +00:00
|
|
|
func (s *Subscription) Next(ctx context.Context) (structs.Events, error) {
|
2020-08-31 17:19:28 +00:00
|
|
|
if atomic.LoadUint32(&s.state) == subscriptionStateClosed {
|
2020-10-04 19:12:35 +00:00
|
|
|
return structs.Events{}, ErrSubscriptionClosed
|
2020-08-31 17:19:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
next, err := s.currentItem.Next(ctx, s.forceClosed)
|
|
|
|
switch {
|
|
|
|
case err != nil && atomic.LoadUint32(&s.state) == subscriptionStateClosed:
|
2020-10-04 19:12:35 +00:00
|
|
|
return structs.Events{}, ErrSubscriptionClosed
|
2020-08-31 17:19:28 +00:00
|
|
|
case err != nil:
|
2020-10-04 19:12:35 +00:00
|
|
|
return structs.Events{}, err
|
2020-10-01 18:43:28 +00:00
|
|
|
}
|
|
|
|
s.currentItem = next
|
|
|
|
|
2020-10-06 20:21:58 +00:00
|
|
|
events := filter(s.req, next.Events.Events)
|
2020-10-01 18:43:28 +00:00
|
|
|
if len(events) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2020-10-06 20:21:58 +00:00
|
|
|
return structs.Events{Index: next.Events.Index, Events: events}, nil
|
2020-10-01 18:43:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-04 19:12:35 +00:00
|
|
|
func (s *Subscription) NextNoBlock() ([]structs.Event, error) {
|
2020-10-01 18:43:28 +00:00
|
|
|
if atomic.LoadUint32(&s.state) == subscriptionStateClosed {
|
|
|
|
return nil, ErrSubscriptionClosed
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
next := s.currentItem.NextNoBlock()
|
|
|
|
if next == nil {
|
|
|
|
return nil, nil
|
2020-08-31 17:19:28 +00:00
|
|
|
}
|
|
|
|
s.currentItem = next
|
|
|
|
|
2020-10-06 20:21:58 +00:00
|
|
|
events := filter(s.req, next.Events.Events)
|
2020-08-31 17:19:28 +00:00
|
|
|
if len(events) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return events, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Subscription) forceClose() {
|
|
|
|
swapped := atomic.CompareAndSwapUint32(&s.state, subscriptionStateOpen, subscriptionStateClosed)
|
|
|
|
if swapped {
|
|
|
|
close(s.forceClosed)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Subscription) Unsubscribe() {
|
|
|
|
s.unsub()
|
|
|
|
}
|
|
|
|
|
2020-10-08 15:57:21 +00:00
|
|
|
// filter events to only those that match a subscriptions topic/keys/namespace
|
2020-10-04 19:12:35 +00:00
|
|
|
func filter(req *SubscribeRequest, events []structs.Event) []structs.Event {
|
2020-08-31 17:19:28 +00:00
|
|
|
if len(events) == 0 {
|
|
|
|
return events
|
|
|
|
}
|
|
|
|
|
|
|
|
var count int
|
|
|
|
for _, e := range events {
|
2020-10-08 18:27:52 +00:00
|
|
|
_, allTopics := req.Topics[structs.TopicAll]
|
2020-09-28 14:13:10 +00:00
|
|
|
if _, ok := req.Topics[e.Topic]; ok || allTopics {
|
|
|
|
var keys []string
|
|
|
|
if allTopics {
|
2020-10-08 18:27:52 +00:00
|
|
|
keys = req.Topics[structs.TopicAll]
|
2020-09-28 14:13:10 +00:00
|
|
|
} else {
|
|
|
|
keys = req.Topics[e.Topic]
|
|
|
|
}
|
2020-10-08 15:57:21 +00:00
|
|
|
if req.Namespace != "" && e.Namespace != "" && e.Namespace != req.Namespace {
|
|
|
|
continue
|
|
|
|
}
|
2020-09-28 14:13:10 +00:00
|
|
|
for _, k := range keys {
|
2020-10-08 18:27:52 +00:00
|
|
|
if e.Key == k || k == string(structs.TopicAll) || filterKeyContains(e.FilterKeys, k) {
|
2020-08-31 17:19:28 +00:00
|
|
|
count++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only allocate a new slice if some events need to be filtered out
|
|
|
|
switch count {
|
|
|
|
case 0:
|
|
|
|
return nil
|
|
|
|
case len(events):
|
|
|
|
return events
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return filtered events
|
2020-10-04 19:12:35 +00:00
|
|
|
result := make([]structs.Event, 0, count)
|
2020-08-31 17:19:28 +00:00
|
|
|
for _, e := range events {
|
2020-10-08 18:27:52 +00:00
|
|
|
_, allTopics := req.Topics[structs.TopicAll]
|
2020-09-28 14:13:10 +00:00
|
|
|
if _, ok := req.Topics[e.Topic]; ok || allTopics {
|
|
|
|
var keys []string
|
|
|
|
if allTopics {
|
2020-10-08 18:27:52 +00:00
|
|
|
keys = req.Topics[structs.TopicAll]
|
2020-09-28 14:13:10 +00:00
|
|
|
} else {
|
|
|
|
keys = req.Topics[e.Topic]
|
|
|
|
}
|
2020-10-08 15:57:21 +00:00
|
|
|
// filter out non matching namespaces
|
|
|
|
if req.Namespace != "" && e.Namespace != "" && e.Namespace != req.Namespace {
|
|
|
|
continue
|
|
|
|
}
|
2020-09-28 14:13:10 +00:00
|
|
|
for _, k := range keys {
|
2020-10-08 18:27:52 +00:00
|
|
|
if e.Key == k || k == string(structs.TopicAll) || filterKeyContains(e.FilterKeys, k) {
|
2020-08-31 17:19:28 +00:00
|
|
|
result = append(result, e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
2020-10-08 18:27:52 +00:00
|
|
|
|
|
|
|
func filterKeyContains(filterKeys []string, key string) bool {
|
|
|
|
for _, fk := range filterKeys {
|
|
|
|
if fk == key {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|