Skip to content

Commit 12210fb

Browse files
mattfungpeterbourgon
authored andcommitted
Add AMQP transport (#746) (#746)
amqp transport publisher amqp transport tests lint fixes for amqp transport fixed formatting and punctuation zzz default ContentType is null, increased max length of correlationId to 255 refractored subscriber EncodeResponseFunc into encode func, and send func zzz
1 parent ba206c1 commit 12210fb

File tree

8 files changed

+1249
-0
lines changed

8 files changed

+1249
-0
lines changed

transport/amqp/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package amqp implements an AMQP transport.
2+
package amqp

transport/amqp/encode-decode.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package amqp
2+
3+
import (
4+
"context"
5+
"github.com/streadway/amqp"
6+
)
7+
8+
// DecodeRequestFunc extracts a user-domain request object from
9+
// an AMQP Delivery object. It is designed to be used in AMQP Subscribers.
10+
type DecodeRequestFunc func(context.Context, *amqp.Delivery) (request interface{}, err error)
11+
12+
// EncodeRequestFunc encodes the passed request object into
13+
// an AMQP Publishing object. It is designed to be used in AMQP Publishers.
14+
type EncodeRequestFunc func(context.Context, *amqp.Publishing, interface{}) error
15+
16+
// EncodeResponseFunc encodes the passed reponse object to
17+
// an AMQP Publishing object. It is designed to be used in AMQP Subscribers.
18+
type EncodeResponseFunc func(context.Context, *amqp.Publishing, interface{}) error
19+
20+
// DecodeResponseFunc extracts a user-domain response object from
21+
// an AMQP Delivery object. It is designed to be used in AMQP Publishers.
22+
type DecodeResponseFunc func(context.Context, *amqp.Delivery) (response interface{}, err error)

transport/amqp/publisher.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package amqp
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/go-kit/kit/endpoint"
8+
"github.com/streadway/amqp"
9+
)
10+
11+
// The golang AMQP implementation requires the []byte representation of
12+
// correlation id strings to have a maximum length of 255 bytes.
13+
const maxCorrelationIdLength = 255
14+
15+
// Publisher wraps an AMQP channel and queue, and provides a method that
16+
// implements endpoint.Endpoint.
17+
type Publisher struct {
18+
ch Channel
19+
q *amqp.Queue
20+
enc EncodeRequestFunc
21+
dec DecodeResponseFunc
22+
before []RequestFunc
23+
after []PublisherResponseFunc
24+
timeout time.Duration
25+
}
26+
27+
// NewPublisher constructs a usable Publisher for a single remote method.
28+
func NewPublisher(
29+
ch Channel,
30+
q *amqp.Queue,
31+
enc EncodeRequestFunc,
32+
dec DecodeResponseFunc,
33+
options ...PublisherOption,
34+
) *Publisher {
35+
p := &Publisher{
36+
ch: ch,
37+
q: q,
38+
enc: enc,
39+
dec: dec,
40+
timeout: 10 * time.Second,
41+
}
42+
for _, option := range options {
43+
option(p)
44+
}
45+
return p
46+
}
47+
48+
// PublisherOption sets an optional parameter for clients.
49+
type PublisherOption func(*Publisher)
50+
51+
// PublisherBefore sets the RequestFuncs that are applied to the outgoing AMQP
52+
// request before it's invoked.
53+
func PublisherBefore(before ...RequestFunc) PublisherOption {
54+
return func(p *Publisher) { p.before = append(p.before, before...) }
55+
}
56+
57+
// PublisherAfter sets the ClientResponseFuncs applied to the incoming AMQP
58+
// request prior to it being decoded. This is useful for obtaining anything off
59+
// of the response and adding onto the context prior to decoding.
60+
func PublisherAfter(after ...PublisherResponseFunc) PublisherOption {
61+
return func(p *Publisher) { p.after = append(p.after, after...) }
62+
}
63+
64+
// PublisherTimeout sets the available timeout for an AMQP request.
65+
func PublisherTimeout(timeout time.Duration) PublisherOption {
66+
return func(p *Publisher) { p.timeout = timeout }
67+
}
68+
69+
// Endpoint returns a usable endpoint that invokes the remote endpoint.
70+
func (p Publisher) Endpoint() endpoint.Endpoint {
71+
return func(ctx context.Context, request interface{}) (interface{}, error) {
72+
ctx, cancel := context.WithTimeout(ctx, p.timeout)
73+
defer cancel()
74+
75+
pub := amqp.Publishing{
76+
ReplyTo: p.q.Name,
77+
CorrelationId: randomString(randInt(5, maxCorrelationIdLength)),
78+
}
79+
80+
if err := p.enc(ctx, &pub, request); err != nil {
81+
return nil, err
82+
}
83+
84+
for _, f := range p.before {
85+
ctx = f(ctx, &pub)
86+
}
87+
88+
deliv, err := p.publishAndConsumeFirstMatchingResponse(ctx, &pub)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
for _, f := range p.after {
94+
ctx = f(ctx, deliv)
95+
}
96+
response, err := p.dec(ctx, deliv)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
return response, nil
102+
}
103+
}
104+
105+
// publishAndConsumeFirstMatchingResponse publishes the specified Publishing
106+
// and returns the first Delivery object with the matching correlationId.
107+
// If the context times out while waiting for a reply, an error will be returned.
108+
func (p Publisher) publishAndConsumeFirstMatchingResponse(
109+
ctx context.Context,
110+
pub *amqp.Publishing,
111+
) (*amqp.Delivery, error) {
112+
err := p.ch.Publish(
113+
getPublishExchange(ctx),
114+
getPublishKey(ctx),
115+
false, //mandatory
116+
false, //immediate
117+
*pub,
118+
)
119+
if err != nil {
120+
return nil, err
121+
}
122+
autoAck := getConsumeAutoAck(ctx)
123+
124+
msg, err := p.ch.Consume(
125+
p.q.Name,
126+
"", //consumer
127+
autoAck,
128+
false, //exclusive
129+
false, //noLocal
130+
false, //noWait
131+
getConsumeArgs(ctx),
132+
)
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
for {
138+
select {
139+
case d := <-msg:
140+
if d.CorrelationId == pub.CorrelationId {
141+
if !autoAck {
142+
d.Ack(false) //multiple
143+
}
144+
return &d, nil
145+
}
146+
147+
case <-ctx.Done():
148+
return nil, ctx.Err()
149+
}
150+
}
151+
152+
}

0 commit comments

Comments
 (0)