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. 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. Short intro to the NATS project…
! ! !
(for context)
4 . 1
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. 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. 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
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. 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. "❗"❗
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. 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. Cancellation in Golang: Background
The Done() channel
https://blog.golang.org/pipelines
13 . 1
14. Cancellation in Golang: Background
The Context interface
https://blog.golang.org/context
14 . 1
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. 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. Cool! ⭐ Now let's add support for Context in the
NATS client.
24 . 1
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. 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. 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. (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. 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. 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. 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
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. 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. 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. 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. 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
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. 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. 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. 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. 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. We could do a pretty advanced usage of the NATS
library without writing a single select or more
goroutines!
45 . 1
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