github.com/xmlking/toolkit/broker/pubsub@v0.3.4/default.go (about) 1 package broker 2 3 import ( 4 "context" 5 "strings" 6 7 "cloud.google.com/go/pubsub" 8 "github.com/cockroachdb/errors" 9 "github.com/rs/zerolog/log" 10 "golang.org/x/sync/errgroup" 11 ) 12 13 const ( 14 DefaultName = "mkit.broker.default" 15 ) 16 17 type pubsubBroker struct { 18 client *pubsub.Client 19 options Options 20 subs []*pubsubSubscriber 21 pubs []*pubsubPublisher 22 } 23 24 type pubsubPublisher struct { 25 options PublishOptions 26 topic *pubsub.Topic 27 } 28 29 func (p *pubsubPublisher) Topic() string { 30 return p.topic.String() 31 } 32 33 // Stop should be called once 34 func (p *pubsubPublisher) stop() { 35 log.Info().Str("component", "pubsub").Msgf("Stopping Publisher: %s", p.Topic()) 36 // It blocks until all items have been flushed. 37 p.topic.Stop() 38 log.Info().Str("component", "pubsub").Msgf("Stopped Publisher Gracefully: %s", p.Topic()) 39 } 40 41 func (p *pubsubPublisher) Publish(ctx context.Context, msg *pubsub.Message) (err error) { 42 pr := p.topic.Publish(ctx, msg) 43 if !p.options.Async { 44 if _, err = pr.Get(ctx); err != nil { 45 log.Error().Err(err).Msgf("Unable to publish to topic: %s", p.topic.String()) 46 } 47 } 48 return 49 } 50 51 type pubsubSubscriber struct { 52 options SubscribeOptions 53 sub *pubsub.Subscription 54 hdlr Handler 55 done chan struct{} 56 } 57 58 func (s *pubsubSubscriber) start(ctx context.Context) (err error) { 59 defer close(s.done) 60 log.Info().Str("component", "pubsub").Msgf("Subscribing to: %s", s.sub) 61 // If ctx is done, Receive returns nil after all of the outstanding calls to `s.hdlr` have returned 62 // and all messages have been acknowledged or have expired. 63 if err = s.sub.Receive(ctx, s.hdlr); err == nil { 64 log.Info().Str("component", "pubsub").Msgf("Stopped Subscriber Gracefully: %s", s.sub) 65 } 66 return 67 } 68 69 func (b *pubsubBroker) NewPublisher(topic string, opts ...PublishOption) (Publisher, error) { 70 t := b.client.Topic(topic) 71 72 options := PublishOptions{ 73 Async: false, 74 } 75 76 for _, o := range opts { 77 o(&options) 78 } 79 80 if exists, err := t.Exists(context.Background()); err != nil { 81 return nil, err 82 } else if !exists { 83 err = errors.Errorf("Doesn't exist Topic: %s", t) 84 return nil, err 85 } 86 87 if options.PublishSettings.DelayThreshold != 0 { 88 t.PublishSettings.DelayThreshold = options.PublishSettings.DelayThreshold 89 } 90 if options.PublishSettings.CountThreshold != 0 { 91 t.PublishSettings.CountThreshold = options.PublishSettings.CountThreshold 92 } 93 if options.PublishSettings.ByteThreshold != 0 { 94 t.PublishSettings.ByteThreshold = options.PublishSettings.ByteThreshold 95 } 96 if options.PublishSettings.NumGoroutines != 0 { 97 t.PublishSettings.NumGoroutines = options.PublishSettings.NumGoroutines 98 } 99 if options.PublishSettings.Timeout != 0 { 100 t.PublishSettings.Timeout = options.PublishSettings.Timeout 101 } 102 if options.PublishSettings.BufferedByteLimit != 0 { 103 t.PublishSettings.BufferedByteLimit = options.PublishSettings.BufferedByteLimit 104 } 105 106 pub := &pubsubPublisher{ 107 topic: t, 108 } 109 // keep track of pubs 110 b.pubs = append(b.pubs, pub) 111 112 return pub, nil 113 } 114 115 // AddSubscriber registers a subscription to the given topic against the google pubsub api 116 func (b *pubsubBroker) AddSubscriber(subscription string, hdlr Handler, opts ...SubscribeOption) error { 117 options := SubscribeOptions{} 118 119 for _, o := range opts { 120 o(&options) 121 } 122 123 sub := b.client.Subscription(subscription) 124 if exists, err := sub.Exists(context.Background()); err != nil { 125 return err 126 } else if !exists { 127 return errors.Errorf("Subscription %s doesn't exists", sub) 128 } 129 130 if options.ReceiveSettings.MaxOutstandingBytes != 0 { 131 sub.ReceiveSettings.MaxOutstandingBytes = options.ReceiveSettings.MaxOutstandingBytes 132 } 133 if options.ReceiveSettings.MaxOutstandingMessages != 0 { 134 sub.ReceiveSettings.MaxOutstandingMessages = options.ReceiveSettings.MaxOutstandingMessages 135 } 136 if options.ReceiveSettings.NumGoroutines != 0 { 137 sub.ReceiveSettings.NumGoroutines = options.ReceiveSettings.NumGoroutines 138 } 139 if options.ReceiveSettings.MaxExtension != 0 { 140 sub.ReceiveSettings.MaxExtension = options.ReceiveSettings.MaxExtension 141 } 142 if options.ReceiveSettings.MaxExtensionPeriod != 0 { 143 sub.ReceiveSettings.MaxExtensionPeriod = options.ReceiveSettings.MaxExtensionPeriod 144 } 145 if options.ReceiveSettings.Synchronous != false { 146 sub.ReceiveSettings.Synchronous = options.ReceiveSettings.Synchronous 147 } 148 149 middleware := hdlr 150 if rHdlr := options.RecoveryHandler; rHdlr != nil { 151 middleware = func(ctx context.Context, msg *pubsub.Message) { 152 defer func() { 153 if r := recover(); r != nil { 154 rHdlr(ctx, msg, r) 155 } 156 }() 157 158 hdlr(ctx, msg) 159 } 160 } 161 162 subscriber := &pubsubSubscriber{ 163 options: options, 164 done: make(chan struct{}), 165 sub: sub, 166 hdlr: middleware, 167 } 168 169 // keep track of subs 170 b.subs = append(b.subs, subscriber) 171 172 return nil 173 } 174 175 // Start is blocking. run as background process. 176 func (b *pubsubBroker) Start() (err error) { 177 ctx := b.options.Context 178 g, egCtx := errgroup.WithContext(ctx) 179 180 // start subscribers in the background. 181 // when context cancelled, they exit without error. 182 for _, sub := range b.subs { 183 g.Go(func() error { 184 return sub.start(egCtx) 185 }) 186 } 187 188 g.Go(func() (err error) { 189 // listen for the interrupt signal 190 <-ctx.Done() 191 192 // log situation 193 switch ctx.Err() { 194 case context.DeadlineExceeded: 195 log.Debug().Str("component", "pubsub").Msg("Context timeout exceeded") 196 case context.Canceled: 197 log.Debug().Str("component", "pubsub").Msg("Context cancelled by interrupt signal") 198 } 199 200 // wait for all subs to stop 201 for _, sub := range b.subs { 202 log.Info().Str("component", "pubsub").Msgf("Stopping Subscriber: %s", sub.sub) 203 <-sub.done 204 } 205 206 // then stop all pubs 207 for _, pub := range b.pubs { 208 pub.stop() 209 } 210 211 // then disconnection client. 212 log.Info().Str("component", "pubsub").Msgf("Closing pubsub client...") 213 err = b.client.Close() 214 215 // Hint: when using pubsub emulator, you receive this error, which you can safely ignore. 216 // Live pubsub server will throw this error. 217 if err != nil && strings.Contains(err.Error(), "the client connection is closing") { 218 err = nil 219 } 220 return 221 }) 222 223 // Wait for all tasks to be finished or return if error occur at any task. 224 return g.Wait() 225 } 226 227 // NewBroker creates a new google pubsub broker 228 func newBroker(ctx context.Context, opts ...Option) Broker { 229 // Default Options 230 options := Options{ 231 Name: DefaultName, 232 Context: ctx, 233 } 234 235 for _, o := range opts { 236 o(&options) 237 } 238 239 // retrieve project id 240 prjID := options.ProjectID 241 242 // if `GOOGLE_CLOUD_PROJECT` is present, it will overwrite programmatically set projectID 243 //if envPrjID := os.Getenv("GOOGLE_CLOUD_PROJECT"); len(envPrjID) > 0 { 244 // prjID = envPrjID 245 //} 246 247 // create pubsub client 248 c, err := pubsub.NewClient(ctx, prjID, options.ClientOptions...) 249 if err != nil { 250 panic(err.Error()) 251 } 252 253 return &pubsubBroker{ 254 client: c, 255 options: options, 256 } 257 }