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 }