Skip to content

Commit 21ccfde

Browse files
Merge pull request #33 from benmoss/deferred-confirmations
Expose a method to enable out-of-order Publisher Confirms
2 parents 6d52470 + 24451ab commit 21ccfde

File tree

5 files changed

+205
-20
lines changed

5 files changed

+205
-20
lines changed

channel.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,8 +1330,19 @@ internal counter for DeliveryTags with the first confirmation starts at 1.
13301330
13311331
*/
13321332
func (ch *Channel) Publish(exchange, key string, mandatory, immediate bool, msg Publishing) error {
1333+
_, err := ch.PublishWithDeferredConfirm(exchange, key, mandatory, immediate, msg)
1334+
return err
1335+
}
1336+
1337+
/*
1338+
PublishWithDeferredConfirm behaves identically to Publish but additionally returns a
1339+
DeferredConfirmation, allowing the caller to wait on the publisher confirmation
1340+
for this message. If the channel has not been put into confirm mode,
1341+
the DeferredConfirmation will be nil.
1342+
*/
1343+
func (ch *Channel) PublishWithDeferredConfirm(exchange, key string, mandatory, immediate bool, msg Publishing) (*DeferredConfirmation, error) {
13331344
if err := msg.Headers.Validate(); err != nil {
1334-
return err
1345+
return nil, err
13351346
}
13361347

13371348
ch.m.Lock()
@@ -1359,14 +1370,14 @@ func (ch *Channel) Publish(exchange, key string, mandatory, immediate bool, msg
13591370
AppId: msg.AppId,
13601371
},
13611372
}); err != nil {
1362-
return err
1373+
return nil, err
13631374
}
13641375

13651376
if ch.confirming {
1366-
ch.confirms.Publish()
1377+
return ch.confirms.Publish(), nil
13671378
}
13681379

1369-
return nil
1380+
return nil, nil
13701381
}
13711382

13721383
/*

client_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,69 @@ func TestConfirmMultipleOrdersDeliveryTags(t *testing.T) {
454454

455455
}
456456

457+
func TestDeferredConfirmations(t *testing.T) {
458+
rwc, srv := newSession(t)
459+
defer rwc.Close()
460+
461+
go func() {
462+
srv.connectionOpen()
463+
srv.channelOpen(1)
464+
465+
srv.recv(1, &confirmSelect{})
466+
srv.send(1, &confirmSelectOk{})
467+
468+
srv.recv(1, &basicPublish{})
469+
srv.recv(1, &basicPublish{})
470+
srv.recv(1, &basicPublish{})
471+
srv.recv(1, &basicPublish{})
472+
}()
473+
474+
c, err := Open(rwc, defaultConfig())
475+
if err != nil {
476+
t.Fatalf("could not create connection: %v (%s)", c, err)
477+
}
478+
479+
ch, err := c.Channel()
480+
if err != nil {
481+
t.Fatalf("could not open channel: %v (%s)", ch, err)
482+
}
483+
484+
ch.Confirm(false)
485+
486+
var results []*DeferredConfirmation
487+
for i := 1; i < 5; i++ {
488+
dc, err := ch.PublishWithDeferredConfirm("", "q", false, false, Publishing{Body: []byte("pub")})
489+
if err != nil {
490+
t.Fatalf("failed to PublishWithDeferredConfirm: %v", err)
491+
}
492+
results = append(results, dc)
493+
}
494+
495+
acks := make(chan Confirmation, 4)
496+
for _, result := range results {
497+
go func(r *DeferredConfirmation) {
498+
acks <- Confirmation{Ack: r.Wait(), DeliveryTag: r.DeliveryTag}
499+
}(result)
500+
}
501+
502+
// received out of order, consumed out of order
503+
assertReceive := func(ack Confirmation, tags ...uint64) {
504+
for _, tag := range tags {
505+
if tag == ack.DeliveryTag {
506+
return
507+
}
508+
}
509+
t.Fatalf("failed ack, expected ack to be in set %v, got %d", tags, ack.DeliveryTag)
510+
}
511+
srv.send(1, &basicAck{DeliveryTag: 2})
512+
assertReceive(<-acks, 2)
513+
srv.send(1, &basicAck{DeliveryTag: 1})
514+
assertReceive(<-acks, 1)
515+
srv.send(1, &basicAck{DeliveryTag: 4, Multiple: true})
516+
assertReceive(<-acks, 3, 4) // 3 and 4 are non-determistic due to map ordering
517+
assertReceive(<-acks, 3, 4)
518+
}
519+
457520
func TestNotifyClosesReusedPublisherConfirmChan(t *testing.T) {
458521
rwc, srv := newSession(t)
459522

confirms.go

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,28 @@
55

66
package amqp091
77

8-
import "sync"
8+
import (
9+
"sync"
10+
)
911

1012
// confirms resequences and notifies one or multiple publisher confirmation listeners
1113
type confirms struct {
12-
m sync.Mutex
13-
listeners []chan Confirmation
14-
sequencer map[uint64]Confirmation
15-
published uint64
16-
publishedMut sync.Mutex
17-
expecting uint64
14+
m sync.Mutex
15+
listeners []chan Confirmation
16+
sequencer map[uint64]Confirmation
17+
deferredConfirmations *deferredConfirmations
18+
published uint64
19+
publishedMut sync.Mutex
20+
expecting uint64
1821
}
1922

2023
// newConfirms allocates a confirms
2124
func newConfirms() *confirms {
2225
return &confirms{
23-
sequencer: map[uint64]Confirmation{},
24-
published: 0,
25-
expecting: 1,
26+
sequencer: map[uint64]Confirmation{},
27+
deferredConfirmations: newDeferredConfirmations(),
28+
published: 0,
29+
expecting: 1,
2630
}
2731
}
2832

@@ -34,12 +38,12 @@ func (c *confirms) Listen(l chan Confirmation) {
3438
}
3539

3640
// Publish increments the publishing counter
37-
func (c *confirms) Publish() uint64 {
41+
func (c *confirms) Publish() *DeferredConfirmation {
3842
c.publishedMut.Lock()
3943
defer c.publishedMut.Unlock()
4044

4145
c.published++
42-
return c.published
46+
return c.deferredConfirmations.Add(c.published)
4347
}
4448

4549
// confirm confirms one publishing, increments the expecting delivery tag, and
@@ -71,6 +75,8 @@ func (c *confirms) One(confirmed Confirmation) {
7175
c.m.Lock()
7276
defer c.m.Unlock()
7377

78+
c.deferredConfirmations.Confirm(confirmed)
79+
7480
if c.expecting == confirmed.DeliveryTag {
7581
c.confirm(confirmed)
7682
} else {
@@ -84,6 +90,8 @@ func (c *confirms) Multiple(confirmed Confirmation) {
8490
c.m.Lock()
8591
defer c.m.Unlock()
8692

93+
c.deferredConfirmations.ConfirmMultiple(confirmed)
94+
8795
for c.expecting <= confirmed.DeliveryTag {
8896
c.confirm(Confirmation{c.expecting, confirmed.Ack})
8997
}
@@ -101,3 +109,56 @@ func (c *confirms) Close() error {
101109
c.listeners = nil
102110
return nil
103111
}
112+
113+
type deferredConfirmations struct {
114+
m sync.Mutex
115+
confirmations map[uint64]*DeferredConfirmation
116+
}
117+
118+
func newDeferredConfirmations() *deferredConfirmations {
119+
return &deferredConfirmations{
120+
confirmations: map[uint64]*DeferredConfirmation{},
121+
}
122+
}
123+
124+
func (d *deferredConfirmations) Add(tag uint64) *DeferredConfirmation {
125+
d.m.Lock()
126+
defer d.m.Unlock()
127+
128+
dc := &DeferredConfirmation{DeliveryTag: tag}
129+
dc.wg.Add(1)
130+
d.confirmations[tag] = dc
131+
return dc
132+
}
133+
134+
func (d *deferredConfirmations) Confirm(confirmation Confirmation) {
135+
d.m.Lock()
136+
defer d.m.Unlock()
137+
138+
dc, found := d.confirmations[confirmation.DeliveryTag]
139+
if !found {
140+
// we should never receive a confirmation for a tag that hasn't been published, but a test causes this to happen
141+
return
142+
}
143+
dc.confirmation = confirmation
144+
dc.wg.Done()
145+
delete(d.confirmations, confirmation.DeliveryTag)
146+
}
147+
148+
func (d *deferredConfirmations) ConfirmMultiple(confirmation Confirmation) {
149+
d.m.Lock()
150+
defer d.m.Unlock()
151+
152+
for k, v := range d.confirmations {
153+
if k <= confirmation.DeliveryTag {
154+
v.confirmation = Confirmation{DeliveryTag: k, Ack: confirmation.Ack}
155+
v.wg.Done()
156+
delete(d.confirmations, k)
157+
}
158+
}
159+
}
160+
161+
func (d *DeferredConfirmation) Wait() bool {
162+
d.wg.Wait()
163+
return d.confirmation.Ack
164+
}

confirms_test.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package amqp091
77

88
import (
9+
"sync"
910
"testing"
1011
"time"
1112
)
@@ -24,8 +25,8 @@ func TestConfirmOneResequences(t *testing.T) {
2425
c.Listen(l)
2526

2627
for i := range fixtures {
27-
if want, got := uint64(i+1), c.Publish(); want != got {
28-
t.Fatalf("expected publish to return the 1 based delivery tag published, want: %d, got: %d", want, got)
28+
if want, got := uint64(i+1), c.Publish(); want != got.DeliveryTag {
29+
t.Fatalf("expected publish to return the 1 based delivery tag published, want: %d, got: %d", want, got.DeliveryTag)
2930
}
3031
}
3132

@@ -139,7 +140,7 @@ func BenchmarkSequentialBufferedConfirms(t *testing.B) {
139140
if i > cap(l)-1 {
140141
<-l
141142
}
142-
c.One(Confirmation{c.Publish(), true})
143+
c.One(Confirmation{c.Publish().DeliveryTag, true})
143144
}
144145
}
145146

@@ -157,7 +158,7 @@ func TestConfirmsIsThreadSafe(t *testing.T) {
157158
c.Listen(l)
158159

159160
for i := 0; i < count; i++ {
160-
go func() { pub <- Confirmation{c.Publish(), true} }()
161+
go func() { pub <- Confirmation{c.Publish().DeliveryTag, true} }()
161162
}
162163

163164
for i := 0; i < count; i++ {
@@ -176,3 +177,42 @@ func TestConfirmsIsThreadSafe(t *testing.T) {
176177
}
177178
}
178179
}
180+
181+
func TestDeferredConfirmationsConfirm(t *testing.T) {
182+
dcs := newDeferredConfirmations()
183+
var wg sync.WaitGroup
184+
for i, ack := range []bool{true, false} {
185+
var result bool
186+
deliveryTag := uint64(i + 1)
187+
dc := dcs.Add(deliveryTag)
188+
wg.Add(1)
189+
go func() {
190+
result = dc.Wait()
191+
wg.Done()
192+
}()
193+
dcs.Confirm(Confirmation{deliveryTag, ack})
194+
wg.Wait()
195+
if result != ack {
196+
t.Fatalf("expected to receive matching ack got %v", result)
197+
}
198+
}
199+
}
200+
201+
func TestDeferredConfirmationsConfirmMultiple(t *testing.T) {
202+
dcs := newDeferredConfirmations()
203+
var wg sync.WaitGroup
204+
var result bool
205+
dc1 := dcs.Add(1)
206+
dc2 := dcs.Add(2)
207+
dc3 := dcs.Add(3)
208+
wg.Add(1)
209+
go func() {
210+
result = dc1.Wait() && dc2.Wait() && dc3.Wait()
211+
wg.Done()
212+
}()
213+
dcs.ConfirmMultiple(Confirmation{4, true})
214+
wg.Wait()
215+
if !result {
216+
t.Fatal("expected to receive true for result, received false")
217+
}
218+
}

types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package amqp091
88
import (
99
"fmt"
1010
"io"
11+
"sync"
1112
"time"
1213
)
1314

@@ -179,6 +180,15 @@ type Blocking struct {
179180
Reason string // Server reason for activation
180181
}
181182

183+
// DeferredConfirmation represents a future publisher confirm for a message. It
184+
// allows users to directly correlate a publishing to a confirmation. These are
185+
// returned from PublishWithDeferredConfirm on Channels.
186+
type DeferredConfirmation struct {
187+
wg sync.WaitGroup
188+
DeliveryTag uint64
189+
confirmation Confirmation
190+
}
191+
182192
// Confirmation notifies the acknowledgment or negative acknowledgement of a
183193
// publishing identified by its delivery tag. Use NotifyPublish on the Channel
184194
// to consume these events.

0 commit comments

Comments
 (0)