debian-mirror-gitlab/workhorse-vendor/gocloud.dev/pubsub/mempubsub/mem.go
2023-01-13 15:02:22 +05:30

377 lines
10 KiB
Go

// Copyright 2018 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package mempubsub provides an in-memory pubsub implementation.
// Use NewTopic to construct a *pubsub.Topic, and/or NewSubscription
// to construct a *pubsub.Subscription.
//
// mempubsub should not be used for production: it is intended for local
// development and testing.
//
// # URLs
//
// For pubsub.OpenTopic and pubsub.OpenSubscription, mempubsub registers
// for the scheme "mem".
// To customize the URL opener, or for more details on the URL format,
// see URLOpener.
// See https://gocloud.dev/concepts/urls/ for background information.
//
// # Message Delivery Semantics
//
// mempubsub supports at-least-once semantics; applications must
// call Message.Ack after processing a message, or it will be redelivered.
// See https://godoc.org/gocloud.dev/pubsub#hdr-At_most_once_and_At_least_once_Delivery
// for more background.
//
// # As
//
// mempubsub does not support any types for As.
package mempubsub // import "gocloud.dev/pubsub/mempubsub"
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"path"
"sync"
"time"
"gocloud.dev/gcerrors"
"gocloud.dev/pubsub"
"gocloud.dev/pubsub/driver"
)
func init() {
o := new(URLOpener)
pubsub.DefaultURLMux().RegisterTopic(Scheme, o)
pubsub.DefaultURLMux().RegisterSubscription(Scheme, o)
}
// Scheme is the URL scheme mempubsub registers its URLOpeners under on pubsub.DefaultMux.
const Scheme = "mem"
// URLOpener opens mempubsub URLs like "mem://topic".
//
// The URL's host+path is used as the topic to create or subscribe to.
//
// Query parameters:
// - ackdeadline: The ack deadline for OpenSubscription, in time.ParseDuration formats.
// Defaults to 1m.
type URLOpener struct {
mu sync.Mutex
topics map[string]*pubsub.Topic
}
// OpenTopicURL opens a pubsub.Topic based on u.
func (o *URLOpener) OpenTopicURL(ctx context.Context, u *url.URL) (*pubsub.Topic, error) {
for param := range u.Query() {
return nil, fmt.Errorf("open topic %v: invalid query parameter %q", u, param)
}
topicName := path.Join(u.Host, u.Path)
o.mu.Lock()
defer o.mu.Unlock()
if o.topics == nil {
o.topics = map[string]*pubsub.Topic{}
}
t := o.topics[topicName]
if t == nil {
t = NewTopic()
o.topics[topicName] = t
}
return t, nil
}
// OpenSubscriptionURL opens a pubsub.Subscription based on u.
func (o *URLOpener) OpenSubscriptionURL(ctx context.Context, u *url.URL) (*pubsub.Subscription, error) {
q := u.Query()
ackDeadline := 1 * time.Minute
if s := q.Get("ackdeadline"); s != "" {
var err error
ackDeadline, err = time.ParseDuration(s)
if err != nil {
return nil, fmt.Errorf("open subscription %v: invalid ackdeadline %q: %v", u, s, err)
}
q.Del("ackdeadline")
}
for param := range q {
return nil, fmt.Errorf("open subscription %v: invalid query parameter %q", u, param)
}
topicName := path.Join(u.Host, u.Path)
o.mu.Lock()
defer o.mu.Unlock()
if o.topics == nil {
o.topics = map[string]*pubsub.Topic{}
}
t := o.topics[topicName]
if t == nil {
return nil, fmt.Errorf("open subscription %v: no topic %q has been created", u, topicName)
}
return NewSubscription(t, ackDeadline), nil
}
var errNotExist = errors.New("mempubsub: topic does not exist")
type topic struct {
mu sync.Mutex
subs []*subscription
nextAckID int
}
// NewTopic creates a new in-memory topic.
func NewTopic() *pubsub.Topic {
return pubsub.NewTopic(&topic{}, nil)
}
// SendBatch implements driver.Topic.SendBatch.
// It is error if the topic is closed or has no subscriptions.
func (t *topic) SendBatch(ctx context.Context, ms []*driver.Message) error {
if err := ctx.Err(); err != nil {
return err
}
if t == nil {
return errNotExist
}
t.mu.Lock()
defer t.mu.Unlock()
// Log a warning if there are no subscribers.
if len(t.subs) == 0 {
log.Print("warning: message sent to topic with no subscribers")
}
// Associate ack IDs with messages here. It would be a bit better if each subscription's
// messages had their own ack IDs, so we could catch one subscription using ack IDs from another,
// but that would require copying all the messages.
for i, m := range ms {
m.AckID = t.nextAckID + i
m.LoggableID = fmt.Sprintf("msg #%d", m.AckID)
m.AsFunc = func(interface{}) bool { return false }
if m.BeforeSend != nil {
if err := m.BeforeSend(func(interface{}) bool { return false }); err != nil {
return err
}
}
if m.AfterSend != nil {
if err := m.AfterSend(func(interface{}) bool { return false }); err != nil {
return err
}
}
}
t.nextAckID += len(ms)
for _, s := range t.subs {
s.add(ms)
}
return nil
}
// IsRetryable implements driver.Topic.IsRetryable.
func (*topic) IsRetryable(error) bool { return false }
// As implements driver.Topic.As.
// It supports *topic so that NewSubscription can recover a *topic
// from the portable type (see below). External users won't be able
// to use As because topic isn't exported.
func (t *topic) As(i interface{}) bool {
x, ok := i.(**topic)
if !ok {
return false
}
*x = t
return true
}
// ErrorAs implements driver.Topic.ErrorAs
func (*topic) ErrorAs(error, interface{}) bool {
return false
}
// ErrorCode implements driver.Topic.ErrorCode
func (*topic) ErrorCode(err error) gcerrors.ErrorCode {
if err == errNotExist {
return gcerrors.NotFound
}
return gcerrors.Unknown
}
// Close implements driver.Topic.Close.
func (*topic) Close() error { return nil }
type subscription struct {
mu sync.Mutex
topic *topic
ackDeadline time.Duration
msgs map[driver.AckID]*message // all unacknowledged messages
}
// NewSubscription creates a new subscription for the given topic.
// It panics if the given topic did not come from mempubsub.
// If a message is not acked within in the given ack deadline from when
// it is received, then it will be redelivered.
func NewSubscription(pstopic *pubsub.Topic, ackDeadline time.Duration) *pubsub.Subscription {
var t *topic
if !pstopic.As(&t) {
panic("mempubsub: NewSubscription passed a Topic not from mempubsub")
}
return pubsub.NewSubscription(newSubscription(t, ackDeadline), nil, nil)
}
func newSubscription(topic *topic, ackDeadline time.Duration) *subscription {
s := &subscription{
topic: topic,
ackDeadline: ackDeadline,
msgs: map[driver.AckID]*message{},
}
if topic != nil {
topic.mu.Lock()
defer topic.mu.Unlock()
topic.subs = append(topic.subs, s)
}
return s
}
type message struct {
msg *driver.Message
expiration time.Time
}
func (s *subscription) add(ms []*driver.Message) {
s.mu.Lock()
defer s.mu.Unlock()
for _, m := range ms {
// The new message will expire at the zero time, which means it will be
// immediately eligible for delivery.
s.msgs[m.AckID] = &message{msg: m}
}
}
// Collect some messages available for delivery. Since we're iterating over a map,
// the order of the messages won't match the publish order, which mimics the actual
// behavior of most pub/sub services.
func (s *subscription) receiveNoWait(now time.Time, max int) []*driver.Message {
var msgs []*driver.Message
s.mu.Lock()
defer s.mu.Unlock()
for _, m := range s.msgs {
if now.After(m.expiration) {
msgs = append(msgs, m.msg)
m.expiration = now.Add(s.ackDeadline)
if len(msgs) == max {
return msgs
}
}
}
return msgs
}
// How long ReceiveBatch should wait if no messages are available, to avoid
// spinning.
const pollDuration = 250 * time.Millisecond
// ReceiveBatch implements driver.ReceiveBatch.
func (s *subscription) ReceiveBatch(ctx context.Context, maxMessages int) ([]*driver.Message, error) {
// Check for closed or cancelled before doing any work.
if err := s.wait(ctx, 0); err != nil {
return nil, err
}
msgs := s.receiveNoWait(time.Now(), maxMessages)
if len(msgs) == 0 {
// When we return no messages and no error, the portable type will call
// ReceiveBatch again immediately. Sleep for a bit to avoid spinning.
time.Sleep(pollDuration)
}
return msgs, nil
}
func (s *subscription) wait(ctx context.Context, dur time.Duration) error {
if s.topic == nil {
return errNotExist
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(dur):
return nil
}
}
// SendAcks implements driver.SendAcks.
func (s *subscription) SendAcks(ctx context.Context, ackIDs []driver.AckID) error {
if s.topic == nil {
return errNotExist
}
// Check for context done before doing any work.
if err := ctx.Err(); err != nil {
return err
}
// Acknowledge messages by removing them from the map.
// Since there is a single map, this correctly handles the case where a message
// is redelivered, but the first receiver acknowledges it.
s.mu.Lock()
defer s.mu.Unlock()
for _, id := range ackIDs {
// It is OK if the message is not in the map; that just means it has been
// previously acked.
delete(s.msgs, id)
}
return nil
}
// CanNack implements driver.CanNack.
func (s *subscription) CanNack() bool { return true }
// SendNacks implements driver.SendNacks.
func (s *subscription) SendNacks(ctx context.Context, ackIDs []driver.AckID) error {
if s.topic == nil {
return errNotExist
}
// Check for context done before doing any work.
if err := ctx.Err(); err != nil {
return err
}
// Nack messages by setting their expiration to the zero time.
s.mu.Lock()
defer s.mu.Unlock()
for _, id := range ackIDs {
if m := s.msgs[id]; m != nil {
m.expiration = time.Time{}
}
}
return nil
}
// IsRetryable implements driver.Subscription.IsRetryable.
func (*subscription) IsRetryable(error) bool { return false }
// As implements driver.Subscription.As.
func (s *subscription) As(i interface{}) bool { return false }
// ErrorAs implements driver.Subscription.ErrorAs
func (*subscription) ErrorAs(error, interface{}) bool {
return false
}
// ErrorCode implements driver.Subscription.ErrorCode
func (*subscription) ErrorCode(err error) gcerrors.ErrorCode {
if err == errNotExist {
return gcerrors.NotFound
}
return gcerrors.Unknown
}
// Close implements driver.Subscription.Close.
func (*subscription) Close() error { return nil }