Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

GopherFest 2017 - Adding Context to NATS

3,025 views

Published on

Using NATS and the Context package together!

Published in: Engineering
  • Be the first to comment

GopherFest 2017 - Adding Context to NATS

  1. 1.   Adding Context to Waldemar Quevedo / GopherFest 2017 @wallyqs 1 . 1
  2. 2. Waldemar Quevedo / So!ware Developer at Development of the Apcera Platform NATS client maintainer (Ruby, Python) Using NATS since 2012 Speaking at GopherCon 2017 :D ABOUT @wallyqs Apcera 2 . 1
  3. 3. ABOUT THIS TALK Quick intro to the NATS project What is Context? When to use it? How we added Context support to the NATS client What can we do with Context and NATS together 3 . 1
  4. 4. Short intro to the NATS project… ! ! ! (for context) 4 . 1
  5. 5. WHAT IS NATS? High Performance Messaging System Open Source, MIT License First written in in 2010 Rewritten in in 2012 Used in production for years at CloudFoundry, Apcera Platform On Github: Website: Ruby Go https://github.com/nats-io http://nats.io/ 5 . 1
  6. 6. WHAT IS NATS? Fast, Simple & Resilient Minimal feature set Pure PubSub (no message persistence*) at-most-once delivery TCP/IP based under a basic plain text protocol (Payload is opaque to protocol)  * see project for at-least once deliveryNATS Streaming 6 . 1
  7. 7. WHAT IS NATS USEFUL FOR? Useful to build control planes for microservices Supports <1:1, 1:N> types of communication Low Latency Request/Response Load balancing via Distribution Queues Great Throughput Above 10M messages per/sec for small payloads Max payload is 1MB by default 7 . 1
  8. 8. EXAMPLES 8 . 1
  9. 9. Publishing API for 1:N communication nc, err := nats.Connect("nats://demo.nats.io:4222") if err != nil { log.Fatalf("Failed to connect: %sn", err) } nc.Subscribe("hello", func(m *nats.Msg) { log.Printf("Received message: %sn", string(m.Data)) }) // Broadcast message to all subscribed to 'hello' err := nc.Publish("hello", []byte("world")) if err != nil { log.Printf("Error publishing payload: %sn", err) } 9 . 1
  10. 10. Request/Response API for 1:1 communication nc, err := nats.Connect("nats://demo.nats.io:4222") if err != nil { log.Fatalf("Failed to connect: %sn", err) } // Receive requests on the 'help' subject... nc.Subscribe("help", func(m *nats.Msg) { log.Printf("Received message: %sn", string(m.Data)) nc.Publish(m.Reply, []byte("ok can help")) }) // Wait for 2 seconds for response or give up result, err := nc.Request("help", []byte("please"), 2*time.Second) if err != nil { log.Printf("Error receiving response: %sn", err) } else { log.Printf("Result: %vn", string(result.Data)) } 10 . 1
  11. 11. "❗"❗ Request/Response API takes a timeout before giving up, blocking until getting either a response or an error. result, err := nc.Request("help", []byte("please"), 2*time.Second) if err != nil { log.Printf("Error receiving response: %sn", err) } 11 . 1
  12. 12. Shortcoming: there is no way to cancel the request! Could this be improved somehow? result, err := nc.Request("help", []byte("please"), 2*time.Second) if err != nil { log.Printf("Error receiving response: %sn", err) } 12 . 1
  13. 13. Cancellation in Golang: Background The Done() channel https://blog.golang.org/pipelines 13 . 1
  14. 14. Cancellation in Golang: Background The Context interface https://blog.golang.org/context 14 . 1
  15. 15. The context package Since Go 1.7, included in standard library ✅ import "context" 15 . 1
  16. 16. Current status Now many library authors are looking into adopting! % % % (GH query) Add context.Context extension:go state:open 16 . 1
  17. 17. 17 . 1
  18. 18. Standard library continuously adopting Context too. ■ net ■ net/http ■ database/sql func (d *Dialer) DialContext(ctx context.Context, network, address string) func (r *Request) WithContext(ctx context.Context) *Request func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error) 18 . 1
  19. 19. TIP If it is a blocking call in a library, it will probably benefit from adding context.Context support soon. 19 . 1
  20. 20. Ok, so how exactly is Context used? 20 . 1
  21. 21. The Context toolkit type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } type Context func Background() Context func TODO() Context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context 21 . 1
  22. 22. HTTP Example req, _ := http.NewRequest("GET", "http://demo.nats.io:8222/varz", nil) // Parent context ctx := context.Background() // Timeout context ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) // Must always call the cancellation function! defer cancel() // Wrap request with the timeout context req = req.WithContext(ctx) result, err := http.DefaultClient.Do(req) if err != nil { log.Fatalf("Error: %sn", err) } 22 . 1
  23. 23. 2 types of errors: ■ context.DeadlineExceeded (← net.Error) ■ context.Canceled Error: Get http://demo.nats.io:8222/varz: context deadline exceeded exit status 1 Error: Get http://demo.nats.io:8222/varz: context canceled exit status 1 23 . 1
  24. 24. Cool! ⭐ Now let's add support for Context in the NATS client. 24 . 1
  25. 25. What is a NATS Request? Essentially, it waits for this to happen in the network: # --> Request SUB _INBOX.ioL1Ws5aZZf5fyeF6sAdjw 2 UNSUB 2 1 PUB help _INBOX.ioL1Ws5aZZf5fyeF6sAdjw 6 please # <-- Response MSG _INBOX.ioL1Ws5aZZf5fyeF6sAdjw 2 11 ok can help 25 . 1
  26. 26. What is a NATS Request? Request API: Syntactic sugar for this under the hood: result, err := nc.Request("help", []byte("please"), 2*time.Second) // Ephemeral subscription for request inbox := NewInbox() s, _ := nc.SubscribeSync(inbox) // Expect single response s.AutoUnsubscribe(1) defer s.Unsubscribe() // Announce request and reply inbox nc.PublishRequest("help", inbox, []byte("please")) // Wait for reply (*blocks here*) msg, _ := s.NextMsg(timeout) 26 . 1
  27. 27. Adding context to NATS Requests First step, add new context aware API for the subscriptions to yield the next message or give up if context is done. // Classic func (s *Subscription) NextMsg(timeout time.Duration) (*Msg, error) // Context Aware API func (s *Subscription) NextMsgWithContext(ctx context.Context) (*Msg, error) 27 . 1
  28. 28. (Just in case) To avoid breaking previous compatibility, add new functionality into a context.go file and add build tag to only support above Go17. // Copyright 2012-2017 Apcera Inc. All rights reserved. // +build go1.7 // A Go client for the NATS messaging system (https://nats.io). package nats import ( "context" ) 28 . 1
  29. 29. Enhancing NextMsg with Context capabilities Without Context (code was simplified): func (s *Subscription) NextMsg(timeout time.Duration) (*Msg, error) { // Subscription channel over which we receive messages mch := s.mch var ok bool var msg *Msg t := time.NewTimer(timeout) // defer t.Stop() // Wait to receive message... select { case msg, ok = <-mch: if !ok { return nil, ErrConnectionClosed } case <-t.C: ' // ...or timer to expire return nil, ErrTimeout } return msg, nil 29 . 1
  30. 30. First pass How about making Subscription context aware? // A Subscription represents interest in a given subject. type Subscription struct { // context used along with the subscription. ctx cntxt // ... func (s *Subscription) NextMsgWithContext(ctx context.Context) (*Msg, error) { // Call NextMsg from subscription but disabling the timeout // as we rely on the context for the cancellation instead. s.SetContext(ctx) msg, err := s.NextMsg(0) if err != nil { select { case <-ctx.Done(): return nil, ctx.Err() default: } } 30 . 1
  31. 31. net/http uses this style type Request struct { // ctx is either the client or server context. It should only // be modified via copying the whole Request using WithContext. // It is unexported to prevent people from using Context wrong // and mutating the contexts held by callers of the same request. ctx context.Context } // WithContext returns a shallow copy of r with its context changed // to ctx. The provided ctx must be non-nil. func (r *Request) WithContext(ctx context.Context) *Request { if ctx == nil { panic("nil context") } r2 := new(Request) *r2 = *r r2.ctx = ctx return r2 } 31 . 1
  32. 32. ❌ style not recommended 32 . 1
  33. 33. Enhancing NextMsg with Context capabilities Without Context (code was simplified): func (s *Subscription) NextMsg(timeout time.Duration) (*Msg, error) { // Subscription channel over which we receive messages mch := s.mch var ok bool var msg *Msg t := time.NewTimer(timeout) // defer t.Stop() // Wait to receive message... select { case msg, ok = <-mch: if !ok { return nil, ErrConnectionClosed } case <-t.C: ' // ...or timer to expire return nil, ErrTimeout } return msg, nil 33 . 1
  34. 34. Enhancing NextMsg with Context capabilities With Context: func (s *Subscription) NextMsgWithContext(ctx context.Context) (*Msg, error) { // Subscription channel over which we receive messages mch := s.mch var ok bool var msg *Msg // Wait to receive message... select { case msg, ok = <-mch: if !ok { return nil, ErrConnectionClosed } case <-ctx.Done(): ' // ...or Context to be done return nil, ctx.Err() } return msg, nil 34 . 1
  35. 35. Learning from the standard library panic in case nil is passed? func (r *Request) WithContext(ctx context.Context) *Request { if ctx == nil { panic("nil context") } func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) if ctx == nil { panic("nil context") } func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { if ctx == nil { panic("nil Context") } 35 . 1
  36. 36. Learning from the standard library panic is not common in the client library so we've added custom error for now instead func (s *Subscription) NextMsgWithContext(ctx context.Context) (*Msg, error) { if ctx == nil { return nil, ErrInvalidContext } 36 . 1
  37. 37. Once we have NextMsgWithContext, we can build on it to add support for RequestWithContext func (nc *Conn) RequestWithContext(ctx context.Context, subj string, data []byte) ( *Msg, error, ) { // Ephemeral subscription for request inbox := NewInbox() s, _ := nc.SubscribeSync(inbox) // Expect single response s.AutoUnsubscribe(1) defer s.Unsubscribe() // Announce request and reply inbox nc.PublishRequest(subj, inbox, data)) // Wait for reply or context to be done return s.NextMsgWithContext(ctx) 37 . 1
  38. 38. And that's it! We now have context.Context support ✌ 38 . 1
  39. 39. NATS Request Example nc, _ := nats.Connect("nats://demo.nats.io:4222") nc.Subscribe("help", func(m *nats.Msg) { log.Printf("Received message: %sn", string(m.Data)) nc.Publish(m.Reply, []byte("ok can help")) }) // Parent context ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() // Blocks until receiving request or context is done result, err := nc.RequestWithContext(ctx, "help", []byte("please")) if err != nil { log.Printf("Error receiving response: %sn", err) } else { log.Printf("Result: %vn", string(result.Data)) } 39 . 1
  40. 40. Cool feature: Cancellation propagation Start from a parent context and chain the rest Opens the door for more advanced use cases without affecting readability too much. context.Background() !"" context.WithDeadline(...) !"" context.WithTimeout(...) 40 . 1
  41. 41. Advanced usage with cancellation Example: Probe Request Goal: Gather as many messages as we can within 1s or until we have been idle without receiving replies for 250ms 41 . 1
  42. 42. Expressed in terms of Context // Parent context ctx := context.Background() // Probing context with hard deadline to 1s ctx, done := context.WithTimeout(ctx, 1*time.Second) defer done() // must be called always // Probe deadline will be expanded as we gather replies timer := time.AfterFunc(250*time.Millisecond, func() { done() }) // ↑ (timer will be reset once a message is received) 42 . 1
  43. 43. Expressed in terms of Context (cont'd) inbox := nats.NewInbox() sub, _ := nc.SubscribeSync(inbox) defer sub.Unsubscribe() start := time.Now() nc.PublishRequest("help", inbox, []byte("please help!")) replies := make([]*Msg, 0) for { // Receive as many messages as we can in 1s or until we stop // receiving new messages for over 250ms. result, err := sub.NextMsgWithContext(ctx) if err != nil { break } replies = append(replies, result) timer.Reset(250 * time.Millisecond) ' // reset timer! * } log.Printf("Received %d messages in %.3f seconds", len(replies), time.Since(start).Secon 43 . 1
  44. 44. Expressed in terms of Context (cont'd) nc.Subscribe("help", func(m *nats.Msg) { log.Printf("Received help request: %sn", string(m.Data)) for i := 0; i < 100; i++ { // Starts to increase latency after a couple of requests if i >= 3 { time.Sleep(300 * time.Millisecond) } nc.Publish(m.Reply, []byte("ok can help")) log.Printf("Replied to help request (times=%d)n", i) time.Sleep(100 * time.Millisecond) } }) 44 . 1
  45. 45. We could do a pretty advanced usage of the NATS library without writing a single select or more goroutines! 45 . 1
  46. 46. CONCLUSIONS If a call blocks in your library, it will probably eventually require context.Context support. Some refactoring might be involved… Catchup with the ecosystem! context.Context based code composes nicely so makes up for very readable code Always call the cancellation function to avoid leaking resources! + 46 . 1
  47. 47. THANKS! / Twitter: github.com/nats-io @nats_io @wallyqs 47 . 1

×