gopherfest 2017 - adding context to nats

47
Adding Context to Waldemar Quevedo / GopherFest 2017 @wallyqs 1.1

Upload: wallyqs

Post on 17-Mar-2018

2.783 views

Category:

Engineering


0 download

TRANSCRIPT

Page 1: GopherFest 2017 -  Adding Context to NATS

 Adding Context to

Waldemar Quevedo /

GopherFest 2017

@wallyqs

1 . 1

Page 2: GopherFest 2017 -  Adding Context to NATS

Waldemar Quevedo / So!ware Developer at

Development of the Apcera PlatformNATS client maintainer (Ruby, Python)Using NATS since 2012Speaking at GopherCon 2017 :D

ABOUT@wallyqs

Apcera

2 . 1

Page 3: GopherFest 2017 -  Adding Context to NATS

ABOUT THIS TALKQuick intro to the NATS projectWhat is Context? When to use it?How we added Context support to the NATS clientWhat can we do with Context and NATS together

3 . 1

Page 4: GopherFest 2017 -  Adding Context to NATS

Short intro to the NATS project…

! ! !(for context)

4 . 1

Page 5: GopherFest 2017 -  Adding Context to NATS

WHAT IS NATS?High Performance Messaging System

Open Source, MIT LicenseFirst written in in 2010

Rewritten in in 2012Used in production for years at

CloudFoundry, Apcera PlatformOn Github: Website:

RubyGo

https://github.com/nats-iohttp://nats.io/

5 . 1

Page 6: GopherFest 2017 -  Adding Context to NATS

WHAT IS NATS?Fast, Simple & ResilientMinimal 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

Page 7: GopherFest 2017 -  Adding Context to NATS

WHAT IS NATS USEFUL FOR?Useful to build control planes for microservicesSupports <1:1, 1:N> types of communication

Low Latency Request/ResponseLoad balancing via Distribution Queues

Great ThroughputAbove 10M messages per/sec for small payloadsMax payload is 1MB by default

7 . 1

Page 8: GopherFest 2017 -  Adding Context to NATS

EXAMPLES

8 . 1

Page 9: GopherFest 2017 -  Adding Context to NATS

Publishing API for 1:N communicationnc, err := nats.Connect("nats://demo.nats.io:4222")if err != nil { log.Fatalf("Failed to connect: %s\n", err)}

nc.Subscribe("hello", func(m *nats.Msg) { log.Printf("Received message: %s\n", string(m.Data))})

// Broadcast message to all subscribed to 'hello'err := nc.Publish("hello", []byte("world"))if err != nil { log.Printf("Error publishing payload: %s\n", err)}

9 . 1

Page 10: GopherFest 2017 -  Adding Context to NATS

Request/Response API for 1:1 communicationnc, err := nats.Connect("nats://demo.nats.io:4222")if err != nil { log.Fatalf("Failed to connect: %s\n", err)}

// Receive requests on the 'help' subject...nc.Subscribe("help", func(m *nats.Msg) { log.Printf("Received message: %s\n", string(m.Data)) nc.Publish(m.Reply, []byte("ok can help"))})

// Wait for 2 seconds for response or give upresult, err := nc.Request("help", []byte("please"), 2*time.Second)if err != nil { log.Printf("Error receiving response: %s\n", err)} else { log.Printf("Result: %v\n", string(result.Data))}

10 . 1

Page 11: GopherFest 2017 -  Adding Context to NATS

"❗"❗Request/Response API takes a timeout before givingup, blocking until getting either a response or anerror.

result, err := nc.Request("help", []byte("please"), 2*time.Second)if err != nil { log.Printf("Error receiving response: %s\n", err)}

11 . 1

Page 12: GopherFest 2017 -  Adding Context to NATS

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: %s\n", err)}

12 . 1

Page 13: GopherFest 2017 -  Adding Context to NATS

Cancellation in Golang: Background

The Done() channel

https://blog.golang.org/pipelines

13 . 1

Page 14: GopherFest 2017 -  Adding Context to NATS

Cancellation in Golang: Background

The Context interface

https://blog.golang.org/context

14 . 1

Page 15: GopherFest 2017 -  Adding Context to NATS

The context package

Since Go 1.7, included in standard library ✅import "context"

15 . 1

Page 16: GopherFest 2017 -  Adding Context to NATS

Current status

Now many library authors are looking into adopting!% % %

(GH query) Add context.Context extension:go state:open

16 . 1

Page 17: GopherFest 2017 -  Adding Context to NATS

17 . 1

Page 18: GopherFest 2017 -  Adding Context to NATS

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

Page 19: GopherFest 2017 -  Adding Context to NATS

TIPIf it is a blocking call in a library, it will probably benefit

from adding context.Context support soon.

19 . 1

Page 20: GopherFest 2017 -  Adding Context to NATS

Ok, so how exactly is Context used?

20 . 1

Page 21: GopherFest 2017 -  Adding Context to NATS

The Context toolkittype 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

Page 22: GopherFest 2017 -  Adding Context to NATS

HTTP Examplereq, _ := http.NewRequest("GET", "http://demo.nats.io:8222/varz", nil)

// Parent contextctx := context.Background()

// Timeout contextctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)

// Must always call the cancellation function!defer cancel()

// Wrap request with the timeout contextreq = req.WithContext(ctx)result, err := http.DefaultClient.Do(req)if err != nil { log.Fatalf("Error: %s\n", err)}

22 . 1

Page 23: GopherFest 2017 -  Adding Context to NATS

2 types of errors:

■ context.DeadlineExceeded (← net.Error)

■ context.Canceled

Error: Get http://demo.nats.io:8222/varz: context deadline exceededexit status 1

Error: Get http://demo.nats.io:8222/varz: context canceledexit status 1

23 . 1

Page 24: GopherFest 2017 -  Adding Context to NATS

Cool! ⭐ Now let's add support for Context in theNATS client.

24 . 1

Page 25: GopherFest 2017 -  Adding Context to NATS

What is a NATS Request?

Essentially, it waits for this to happen in the network:# --> RequestSUB _INBOX.ioL1Ws5aZZf5fyeF6sAdjw 2UNSUB 2 1PUB help _INBOX.ioL1Ws5aZZf5fyeF6sAdjw 6please# <-- ResponseMSG _INBOX.ioL1Ws5aZZf5fyeF6sAdjw 2 11ok can help

25 . 1

Page 26: GopherFest 2017 -  Adding Context to NATS

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 requestinbox := NewInbox()s, _ := nc.SubscribeSync(inbox)

// Expect single responses.AutoUnsubscribe(1)defer s.Unsubscribe()

// Announce request and reply inboxnc.PublishRequest("help", inbox, []byte("please"))

// Wait for reply (*blocks here*)msg, _ := s.NextMsg(timeout)

26 . 1

Page 27: GopherFest 2017 -  Adding Context to NATS

Adding context to NATS Requests

First step, add new context aware API for thesubscriptions to yield the next message or give up ifcontext is done.// Classicfunc (s *Subscription) NextMsg(timeout time.Duration) (*Msg, error)

// Context Aware APIfunc (s *Subscription) NextMsgWithContext(ctx context.Context) (*Msg, error)

27 . 1

Page 28: GopherFest 2017 -  Adding Context to NATS

(Just in case) To avoid breaking previous compatibility,add new functionality into a context.go file andadd 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

Page 29: GopherFest 2017 -  Adding Context to NATS

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

Page 30: GopherFest 2017 -  Adding Context to NATS

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

Page 31: GopherFest 2017 -  Adding Context to NATS

net/http uses this styletype 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

Page 32: GopherFest 2017 -  Adding Context to NATS

❌ style not recommended

32 . 1

Page 33: GopherFest 2017 -  Adding Context to NATS

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

Page 34: GopherFest 2017 -  Adding Context to NATS

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

Page 35: GopherFest 2017 -  Adding Context to NATS

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

Page 36: GopherFest 2017 -  Adding Context to NATS

Learning from the standard library

panic is not common in the client library so we'veadded custom error for now insteadfunc (s *Subscription) NextMsgWithContext(ctx context.Context) (*Msg, error) { if ctx == nil { return nil, ErrInvalidContext }

36 . 1

Page 37: GopherFest 2017 -  Adding Context to NATS

Once we have NextMsgWithContext, we can buildon it to add support for RequestWithContextfunc (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

Page 38: GopherFest 2017 -  Adding Context to NATS

And that's it!

We now have context.Context support ✌

38 . 1

Page 39: GopherFest 2017 -  Adding Context to NATS

NATS Request Examplenc, _ := nats.Connect("nats://demo.nats.io:4222")nc.Subscribe("help", func(m *nats.Msg) { log.Printf("Received message: %s\n", string(m.Data)) nc.Publish(m.Reply, []byte("ok can help"))})

// Parent contextctx := context.Background()ctx, cancel := context.WithTimeout(ctx, 2*time.Second)defer cancel()

// Blocks until receiving request or context is doneresult, err := nc.RequestWithContext(ctx, "help", []byte("please"))if err != nil { log.Printf("Error receiving response: %s\n", err)} else { log.Printf("Result: %v\n", string(result.Data))}

39 . 1

Page 40: GopherFest 2017 -  Adding Context to NATS

Cool feature: Cancellation propagation

Start from a parent context and chain the rest

Opens the door for more advanced use cases withoutaffecting readability too much.

context.Background()!"" context.WithDeadline(...) !"" context.WithTimeout(...)

40 . 1

Page 41: GopherFest 2017 -  Adding Context to NATS

Advanced usage with cancellation

Example: Probe Request

Goal: Gather as many messages as we can within 1s oruntil we have been idle without receiving replies for250ms

41 . 1

Page 42: GopherFest 2017 -  Adding Context to NATS

Expressed in terms of Context// Parent contextctx := context.Background()

// Probing context with hard deadline to 1sctx, done := context.WithTimeout(ctx, 1*time.Second)defer done() // must be called always

// Probe deadline will be expanded as we gather repliestimer := time.AfterFunc(250*time.Millisecond, func() { done()})// ↑ (timer will be reset once a message is received)

42 . 1

Page 43: GopherFest 2017 -  Adding Context to NATS

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).Seconds())

43 . 1

Page 44: GopherFest 2017 -  Adding Context to NATS

Expressed in terms of Context (cont'd)nc.Subscribe("help", func(m *nats.Msg) { log.Printf("Received help request: %s\n", 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

Page 45: GopherFest 2017 -  Adding Context to NATS

We could do a pretty advanced usage of the NATSlibrary without writing a single select or more

goroutines!

45 . 1

Page 46: GopherFest 2017 -  Adding Context to NATS

CONCLUSIONSIf a call blocks in your library, it will probablyeventually require context.Context support.

Some refactoring might be involved…Catchup with the ecosystem!

context.Context based code composes nicelyso makes up for very readable codeAlways call the cancellation function to avoidleaking resources! +

46 . 1

Page 47: GopherFest 2017 -  Adding Context to NATS

THANKS! /

Twitter:

github.com/nats-io @nats_io

@wallyqs

47 . 1