github.com/thiagoyeds/go-cloud@v0.26.0/pubsub/mempubsub/mem.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package mempubsub provides an in-memory pubsub implementation.
    16  // Use NewTopic to construct a *pubsub.Topic, and/or NewSubscription
    17  // to construct a *pubsub.Subscription.
    18  //
    19  // mempubsub should not be used for production: it is intended for local
    20  // development and testing.
    21  //
    22  // URLs
    23  //
    24  // For pubsub.OpenTopic and pubsub.OpenSubscription, mempubsub registers
    25  // for the scheme "mem".
    26  // To customize the URL opener, or for more details on the URL format,
    27  // see URLOpener.
    28  // See https://gocloud.dev/concepts/urls/ for background information.
    29  //
    30  // Message Delivery Semantics
    31  //
    32  // mempubsub supports at-least-once semantics; applications must
    33  // call Message.Ack after processing a message, or it will be redelivered.
    34  // See https://godoc.org/gocloud.dev/pubsub#hdr-At_most_once_and_At_least_once_Delivery
    35  // for more background.
    36  //
    37  // As
    38  //
    39  // mempubsub does not support any types for As.
    40  package mempubsub // import "gocloud.dev/pubsub/mempubsub"
    41  
    42  import (
    43  	"context"
    44  	"errors"
    45  	"fmt"
    46  	"log"
    47  	"net/url"
    48  	"path"
    49  	"sync"
    50  	"time"
    51  
    52  	"gocloud.dev/gcerrors"
    53  	"gocloud.dev/pubsub"
    54  	"gocloud.dev/pubsub/driver"
    55  )
    56  
    57  func init() {
    58  	o := new(URLOpener)
    59  	pubsub.DefaultURLMux().RegisterTopic(Scheme, o)
    60  	pubsub.DefaultURLMux().RegisterSubscription(Scheme, o)
    61  }
    62  
    63  // Scheme is the URL scheme mempubsub registers its URLOpeners under on pubsub.DefaultMux.
    64  const Scheme = "mem"
    65  
    66  // URLOpener opens mempubsub URLs like "mem://topic".
    67  //
    68  // The URL's host+path is used as the topic to create or subscribe to.
    69  //
    70  // Query parameters:
    71  //   - ackdeadline: The ack deadline for OpenSubscription, in time.ParseDuration formats.
    72  //       Defaults to 1m.
    73  type URLOpener struct {
    74  	mu     sync.Mutex
    75  	topics map[string]*pubsub.Topic
    76  }
    77  
    78  // OpenTopicURL opens a pubsub.Topic based on u.
    79  func (o *URLOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*pubsub.Topic, error) {
    80  	for param := range u.Query() {
    81  		return nil, fmt.Errorf("open topic %v: invalid query parameter %q", u, param)
    82  	}
    83  	topicName := path.Join(u.Host, u.Path)
    84  	o.mu.Lock()
    85  	defer o.mu.Unlock()
    86  	if o.topics == nil {
    87  		o.topics = map[string]*pubsub.Topic{}
    88  	}
    89  	t := o.topics[topicName]
    90  	if t == nil {
    91  		t = NewTopic()
    92  		o.topics[topicName] = t
    93  	}
    94  	return t, nil
    95  }
    96  
    97  // OpenSubscriptionURL opens a pubsub.Subscription based on u.
    98  func (o *URLOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*pubsub.Subscription, error) {
    99  	q := u.Query()
   100  
   101  	ackDeadline := 1 * time.Minute
   102  	if s := q.Get("ackdeadline"); s != "" {
   103  		var err error
   104  		ackDeadline, err = time.ParseDuration(s)
   105  		if err != nil {
   106  			return nil, fmt.Errorf("open subscription %v: invalid ackdeadline %q: %v", u, s, err)
   107  		}
   108  		q.Del("ackdeadline")
   109  	}
   110  	for param := range q {
   111  		return nil, fmt.Errorf("open subscription %v: invalid query parameter %q", u, param)
   112  	}
   113  	topicName := path.Join(u.Host, u.Path)
   114  	o.mu.Lock()
   115  	defer o.mu.Unlock()
   116  	if o.topics == nil {
   117  		o.topics = map[string]*pubsub.Topic{}
   118  	}
   119  	t := o.topics[topicName]
   120  	if t == nil {
   121  		return nil, fmt.Errorf("open subscription %v: no topic %q has been created", u, topicName)
   122  	}
   123  	return NewSubscription(t, ackDeadline), nil
   124  }
   125  
   126  var errNotExist = errors.New("mempubsub: topic does not exist")
   127  
   128  type topic struct {
   129  	mu        sync.Mutex
   130  	subs      []*subscription
   131  	nextAckID int
   132  }
   133  
   134  // NewTopic creates a new in-memory topic.
   135  func NewTopic() *pubsub.Topic {
   136  	return pubsub.NewTopic(&topic{}, nil)
   137  }
   138  
   139  // SendBatch implements driver.Topic.SendBatch.
   140  // It is error if the topic is closed or has no subscriptions.
   141  func (t *topic) SendBatch(ctx context.Context, ms []*driver.Message) error {
   142  	if err := ctx.Err(); err != nil {
   143  		return err
   144  	}
   145  	if t == nil {
   146  		return errNotExist
   147  	}
   148  	t.mu.Lock()
   149  	defer t.mu.Unlock()
   150  
   151  	// Log a warning if there are no subscribers.
   152  	if len(t.subs) == 0 {
   153  		log.Print("warning: message sent to topic with no subscribers")
   154  	}
   155  
   156  	// Associate ack IDs with messages here. It would be a bit better if each subscription's
   157  	// messages had their own ack IDs, so we could catch one subscription using ack IDs from another,
   158  	// but that would require copying all the messages.
   159  	for i, m := range ms {
   160  		m.AckID = t.nextAckID + i
   161  		m.LoggableID = fmt.Sprintf("msg #%d", m.AckID)
   162  		m.AsFunc = func(interface{}) bool { return false }
   163  
   164  		if m.BeforeSend != nil {
   165  			if err := m.BeforeSend(func(interface{}) bool { return false }); err != nil {
   166  				return err
   167  			}
   168  		}
   169  		if m.AfterSend != nil {
   170  			if err := m.AfterSend(func(interface{}) bool { return false }); err != nil {
   171  				return err
   172  			}
   173  		}
   174  	}
   175  	t.nextAckID += len(ms)
   176  	for _, s := range t.subs {
   177  		s.add(ms)
   178  	}
   179  	return nil
   180  }
   181  
   182  // IsRetryable implements driver.Topic.IsRetryable.
   183  func (*topic) IsRetryable(error) bool { return false }
   184  
   185  // As implements driver.Topic.As.
   186  // It supports *topic so that NewSubscription can recover a *topic
   187  // from the portable type (see below). External users won't be able
   188  // to use As because topic isn't exported.
   189  func (t *topic) As(i interface{}) bool {
   190  	x, ok := i.(**topic)
   191  	if !ok {
   192  		return false
   193  	}
   194  	*x = t
   195  	return true
   196  }
   197  
   198  // ErrorAs implements driver.Topic.ErrorAs
   199  func (*topic) ErrorAs(error, interface{}) bool {
   200  	return false
   201  }
   202  
   203  // ErrorCode implements driver.Topic.ErrorCode
   204  func (*topic) ErrorCode(err error) gcerrors.ErrorCode {
   205  	if err == errNotExist {
   206  		return gcerrors.NotFound
   207  	}
   208  	return gcerrors.Unknown
   209  }
   210  
   211  // Close implements driver.Topic.Close.
   212  func (*topic) Close() error { return nil }
   213  
   214  type subscription struct {
   215  	mu          sync.Mutex
   216  	topic       *topic
   217  	ackDeadline time.Duration
   218  	msgs        map[driver.AckID]*message // all unacknowledged messages
   219  }
   220  
   221  // NewSubscription creates a new subscription for the given topic.
   222  // It panics if the given topic did not come from mempubsub.
   223  // If a message is not acked within in the given ack deadline from when
   224  // it is received, then it will be redelivered.
   225  func NewSubscription(pstopic *pubsub.Topic, ackDeadline time.Duration) *pubsub.Subscription {
   226  	var t *topic
   227  	if !pstopic.As(&t) {
   228  		panic("mempubsub: NewSubscription passed a Topic not from mempubsub")
   229  	}
   230  	return pubsub.NewSubscription(newSubscription(t, ackDeadline), nil, nil)
   231  }
   232  
   233  func newSubscription(topic *topic, ackDeadline time.Duration) *subscription {
   234  	s := &subscription{
   235  		topic:       topic,
   236  		ackDeadline: ackDeadline,
   237  		msgs:        map[driver.AckID]*message{},
   238  	}
   239  	if topic != nil {
   240  		topic.mu.Lock()
   241  		defer topic.mu.Unlock()
   242  		topic.subs = append(topic.subs, s)
   243  	}
   244  	return s
   245  }
   246  
   247  type message struct {
   248  	msg        *driver.Message
   249  	expiration time.Time
   250  }
   251  
   252  func (s *subscription) add(ms []*driver.Message) {
   253  	s.mu.Lock()
   254  	defer s.mu.Unlock()
   255  	for _, m := range ms {
   256  		// The new message will expire at the zero time, which means it will be
   257  		// immediately eligible for delivery.
   258  		s.msgs[m.AckID] = &message{msg: m}
   259  	}
   260  }
   261  
   262  // Collect some messages available for delivery. Since we're iterating over a map,
   263  // the order of the messages won't match the publish order, which mimics the actual
   264  // behavior of most pub/sub services.
   265  func (s *subscription) receiveNoWait(now time.Time, max int) []*driver.Message {
   266  	var msgs []*driver.Message
   267  	s.mu.Lock()
   268  	defer s.mu.Unlock()
   269  	for _, m := range s.msgs {
   270  		if now.After(m.expiration) {
   271  			msgs = append(msgs, m.msg)
   272  			m.expiration = now.Add(s.ackDeadline)
   273  			if len(msgs) == max {
   274  				return msgs
   275  			}
   276  		}
   277  	}
   278  	return msgs
   279  }
   280  
   281  // How long ReceiveBatch should wait if no messages are available, to avoid
   282  // spinning.
   283  const pollDuration = 250 * time.Millisecond
   284  
   285  // ReceiveBatch implements driver.ReceiveBatch.
   286  func (s *subscription) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
   287  	// Check for closed or cancelled before doing any work.
   288  	if err := s.wait(ctx, 0); err != nil {
   289  		return nil, err
   290  	}
   291  	msgs := s.receiveNoWait(time.Now(), maxMessages)
   292  	if len(msgs) == 0 {
   293  		// When we return no messages and no error, the portable type will call
   294  		// ReceiveBatch again immediately. Sleep for a bit to avoid spinning.
   295  		time.Sleep(pollDuration)
   296  	}
   297  	return msgs, nil
   298  }
   299  
   300  func (s *subscription) wait(ctx context.Context, dur time.Duration) error {
   301  	if s.topic == nil {
   302  		return errNotExist
   303  	}
   304  	select {
   305  	case <-ctx.Done():
   306  		return ctx.Err()
   307  	case <-time.After(dur):
   308  		return nil
   309  	}
   310  }
   311  
   312  // SendAcks implements driver.SendAcks.
   313  func (s *subscription) SendAcks(ctx context.Context, ackIDs []driver.AckID) error {
   314  	if s.topic == nil {
   315  		return errNotExist
   316  	}
   317  	// Check for context done before doing any work.
   318  	if err := ctx.Err(); err != nil {
   319  		return err
   320  	}
   321  	// Acknowledge messages by removing them from the map.
   322  	// Since there is a single map, this correctly handles the case where a message
   323  	// is redelivered, but the first receiver acknowledges it.
   324  	s.mu.Lock()
   325  	defer s.mu.Unlock()
   326  	for _, id := range ackIDs {
   327  		// It is OK if the message is not in the map; that just means it has been
   328  		// previously acked.
   329  		delete(s.msgs, id)
   330  	}
   331  	return nil
   332  }
   333  
   334  // CanNack implements driver.CanNack.
   335  func (s *subscription) CanNack() bool { return true }
   336  
   337  // SendNacks implements driver.SendNacks.
   338  func (s *subscription) SendNacks(ctx context.Context, ackIDs []driver.AckID) error {
   339  	if s.topic == nil {
   340  		return errNotExist
   341  	}
   342  	// Check for context done before doing any work.
   343  	if err := ctx.Err(); err != nil {
   344  		return err
   345  	}
   346  	// Nack messages by setting their expiration to the zero time.
   347  	s.mu.Lock()
   348  	defer s.mu.Unlock()
   349  	for _, id := range ackIDs {
   350  		if m := s.msgs[id]; m != nil {
   351  			m.expiration = time.Time{}
   352  		}
   353  	}
   354  	return nil
   355  }
   356  
   357  // IsRetryable implements driver.Subscription.IsRetryable.
   358  func (*subscription) IsRetryable(error) bool { return false }
   359  
   360  // As implements driver.Subscription.As.
   361  func (s *subscription) As(i interface{}) bool { return false }
   362  
   363  // ErrorAs implements driver.Subscription.ErrorAs
   364  func (*subscription) ErrorAs(error, interface{}) bool {
   365  	return false
   366  }
   367  
   368  // ErrorCode implements driver.Subscription.ErrorCode
   369  func (*subscription) ErrorCode(err error) gcerrors.ErrorCode {
   370  	if err == errNotExist {
   371  		return gcerrors.NotFound
   372  	}
   373  	return gcerrors.Unknown
   374  }
   375  
   376  // Close implements driver.Subscription.Close.
   377  func (*subscription) Close() error { return nil }