1
0
mirror of https://github.com/go-mqtt/mqtt.git synced 2025-08-08 22:42:05 +03:00

Error scenario tuning including new ErrBoke case.

This commit is contained in:
Pascal S. de Kloe
2021-02-20 16:59:39 +01:00
parent c1e51592da
commit 5d0687fbd6
6 changed files with 139 additions and 116 deletions

View File

@@ -24,7 +24,7 @@ var readBufSize = 128 * 1024
var ErrDown = errors.New("mqtt: connection unavailable") var ErrDown = errors.New("mqtt: connection unavailable")
// ErrClosed signals use after Close. The state is permanent. // ErrClosed signals use after Close. The state is permanent.
// Further invocation will again result in the same error. // Further invocation will result again in an ErrClosed error.
var ErrClosed = errors.New("mqtt: client closed") var ErrClosed = errors.New("mqtt: client closed")
// ErrBrokerTerm signals connection loss for unknown reasons. // ErrBrokerTerm signals connection loss for unknown reasons.
@@ -462,12 +462,12 @@ func (c *Client) termCallbacks() {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
c.unorderedTxs.close() c.unorderedTxs.breakAll()
}() }()
select { select {
case ack := <-c.pingAck: case ack := <-c.pingAck:
ack <- ErrClosed ack <- ErrBreak
default: default:
break break
} }
@@ -537,6 +537,14 @@ func (c *Client) toOffline() {
default: default:
c.onlineSig <- on c.onlineSig <- on
} }
select {
case ack := <-c.pingAck:
ack <- ErrBreak
default:
break
}
c.unorderedTxs.breakAll()
} }
func (c *Client) lockWrite(quit <-chan struct{}) (net.Conn, error) { func (c *Client) lockWrite(quit <-chan struct{}) (net.Conn, error) {

View File

@@ -282,45 +282,37 @@ func TestDown(t *testing.T) {
if !errors.Is(err, mqtt.ErrDown) { if !errors.Is(err, mqtt.ErrDown) {
t.Errorf("PublishRetained round %d got error %q, want an ErrDown", roundN, err) t.Errorf("PublishRetained round %d got error %q, want an ErrDown", roundN, err)
} }
ack, err := client.PublishAtLeastOnce(nil, "x") _, err = client.PublishAtLeastOnce(nil, "x")
if roundN > 1 { if roundN > 1 {
if !errors.Is(err, mqtt.ErrMax) { if !errors.Is(err, mqtt.ErrMax) {
t.Errorf("PublishAtLeastOnce round %d got error %q, want an ErrMax", roundN, err) t.Errorf("PublishAtLeastOnce round %d got error %q, want an ErrMax", roundN, err)
} }
} else if err != nil { } else if err != nil {
t.Errorf("PublishAtLeastOnce round %d got error %q", roundN, err) t.Errorf("PublishAtLeastOnce round %d got error %q", roundN, err)
} else if err := <-ack; !errors.Is(err, mqtt.ErrDown) {
t.Errorf("PublishAtLeastOnce round %d ack got error %q, want an ErrDown", roundN, err)
} }
ack, err = client.PublishAtLeastOnceRetained(nil, "x") _, err = client.PublishAtLeastOnceRetained(nil, "x")
if roundN > 1 { if roundN > 1 {
if !errors.Is(err, mqtt.ErrMax) { if !errors.Is(err, mqtt.ErrMax) {
t.Errorf("PublishAtLeastOnceRetained round %d got error %q, want an ErrMax", roundN, err) t.Errorf("PublishAtLeastOnceRetained round %d got error %q, want an ErrMax", roundN, err)
} }
} else if err != nil { } else if err != nil {
t.Errorf("PublishAtLeastOnceRetained round %d got error %q", roundN, err) t.Errorf("PublishAtLeastOnceRetained round %d got error %q", roundN, err)
} else if err := <-ack; !errors.Is(err, mqtt.ErrDown) {
t.Errorf("PublishAtLeastOnceRetained round %d ack got error %q, want an ErrDown", roundN, err)
} }
ack, err = client.PublishExactlyOnce(nil, "x") _, err = client.PublishExactlyOnce(nil, "x")
if roundN > 1 { if roundN > 1 {
if !errors.Is(err, mqtt.ErrMax) { if !errors.Is(err, mqtt.ErrMax) {
t.Errorf("PublishExactlyOnce round %d got error %q, want an ErrMax", roundN, err) t.Errorf("PublishExactlyOnce round %d got error %q, want an ErrMax", roundN, err)
} }
} else if err != nil { } else if err != nil {
t.Errorf("PublishExactlyOnce round %d got error %q", roundN, err) t.Errorf("PublishExactlyOnce round %d got error %q", roundN, err)
} else if err := <-ack; !errors.Is(err, mqtt.ErrDown) {
t.Errorf("PublishExactlyOnce round %d ack got error %q, want an ErrDown", roundN, err)
} }
ack, err = client.PublishExactlyOnceRetained(nil, "x") _, err = client.PublishExactlyOnceRetained(nil, "x")
if roundN > 1 { if roundN > 1 {
if !errors.Is(err, mqtt.ErrMax) { if !errors.Is(err, mqtt.ErrMax) {
t.Errorf("PublishExactlyOnceRetained round %d got error %q, want an ErrMax", roundN, err) t.Errorf("PublishExactlyOnceRetained round %d got error %q, want an ErrMax", roundN, err)
} }
} else if err != nil { } else if err != nil {
t.Errorf("PublishExactlyOnceRetained round %d got error %q", roundN, err) t.Errorf("PublishExactlyOnceRetained round %d got error %q", roundN, err)
} else if err := <-ack; !errors.Is(err, mqtt.ErrDown) {
t.Errorf("PublishExactlyOnceRetained round %d ack got error %q, want an ErrDown", roundN, err)
} }
err = client.Ping(nil) err = client.Ping(nil)
if !errors.Is(err, mqtt.ErrDown) { if !errors.Is(err, mqtt.ErrDown) {

View File

@@ -29,18 +29,14 @@ var Online func() <-chan struct{}
func init() { func init() {
PublishAtLeastOnce = mqtttest.NewPublishAckStub(nil) PublishAtLeastOnce = mqtttest.NewPublishAckStub(nil)
Subscribe = mqtttest.NewSubscribeStub(nil) Subscribe = mqtttest.NewSubscribeStub(nil)
Online = func() <-chan struct{} { Online = func() <-chan struct{} { return nil }
ch := make(chan struct{})
close(ch)
return ch
}
} }
// It is good practice to install the client from main. // It is good practice to install the client from main.
func ExampleClient_setup() { func ExampleClient_setup() {
client, err := mqtt.VolatileSession("demo-client", &mqtt.Config{ client, err := mqtt.VolatileSession("demo-client", &mqtt.Config{
Dialer: mqtt.NewDialer("tcp", "localhost:1883"), Dialer: mqtt.NewDialer("tcp", "localhost:1883"),
WireTimeout: time.Second, WireTimeout: 4 * time.Second,
}) })
if err != nil { if err != nil {
log.Fatal("exit on broken setup: ", err) log.Fatal("exit on broken setup: ", err)
@@ -50,27 +46,27 @@ func ExampleClient_setup() {
go func() { go func() {
var big *mqtt.BigMessage var big *mqtt.BigMessage
for { for {
message, channel, err := client.ReadSlices() message, topic, err := client.ReadSlices()
switch { switch {
case err == nil: case err == nil:
// do something with inbound message // do something with inbound message
log.Printf("📥 %q: %q", channel, message) log.Printf("📥 %q: %q", topic, message)
case errors.As(err, &big):
log.Printf("📥 %q: %d byte message omitted", big.Topic, big.Size)
case errors.Is(err, mqtt.ErrClosed): case errors.Is(err, mqtt.ErrClosed):
log.Print(err) log.Print(err)
return // terminated return // terminated
case errors.As(err, &big):
log.Printf("%d byte content skipped", big.Size)
case mqtt.IsConnectionRefused(err): case mqtt.IsConnectionRefused(err):
log.Print(err) log.Print("queue unavailable: ", err)
// ErrDown for a while // ErrDown for a while
time.Sleep(15*time.Minute - time.Second) time.Sleep(15 * time.Minute)
default: default:
log.Print("MQTT unavailable: ", err) log.Print("queue unavailable: ", err)
// ErrDown for short backoff // ErrDown during backoff
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
} }
@@ -106,83 +102,110 @@ func ExampleClient_setup() {
// Output: // Output:
} }
// Error scenario and how to act uppon them. // Demonstrates all error scenario and the respective recovery options.
func ExampleClient_PublishAtLeastOnce_hasty() { func ExampleClient_PublishAtLeastOnce_critical() {
for { for {
ack, err := PublishAtLeastOnce([]byte("🍸🆘"), "demo/alert") exchange, err := PublishAtLeastOnce([]byte("🍸🆘"), "demo/alert")
switch { switch {
case err == nil: case err == nil:
fmt.Println("alert submitted") fmt.Println("alert submitted")
break
case mqtt.IsDeny(err), errors.Is(err, mqtt.ErrClosed): case mqtt.IsDeny(err), errors.Is(err, mqtt.ErrClosed):
fmt.Println("🚨 alert not send:", err) fmt.Println("🚨 alert not send:", err)
return return
case errors.Is(err, mqtt.ErrDown):
fmt.Println("⚠️ alert delay:", err)
<-Online()
case errors.Is(err, mqtt.ErrMax): case errors.Is(err, mqtt.ErrMax):
fmt.Println("⚠️ alert delay:", err) fmt.Println("⚠️ alert submission hold-up:", err)
time.Sleep(time.Second / 4) time.Sleep(time.Second / 4)
continue continue
default: default:
fmt.Println("⚠️ alert delay on persistence malfunction:", err) fmt.Println("⚠️ alert submission blocked on persistence malfunction:", err)
time.Sleep(time.Second) time.Sleep(4 * time.Second)
continue continue
} }
for err := range ack { for err := range exchange {
if errors.Is(err, mqtt.ErrClosed) { if errors.Is(err, mqtt.ErrClosed) {
fmt.Println("🚨 alert suspended:", err) fmt.Println("🚨 alert exchange suspended:", err)
// Submission will continue when the Client // An AdoptSession may continue the transaction.
// is restarted with the same Store again.
return return
} }
fmt.Println("⚠️ alert delay on connection malfunction:", err)
fmt.Println("⚠️ alert request transfer interupted:", err)
} }
fmt.Println("alert confirmed") fmt.Println("alert acknowledged ✓")
break break
} }
// Output: // Output:
// alert submitted // alert submitted
// alert confirmed // alert acknowledged ✓
} }
// Error scenario and how to act uppon them. // Demonstrates all error scenario and the respective recovery options.
func ExampleClient_Subscribe_sticky() { func ExampleClient_Subscribe_sticky() {
const topicFilter = "demo/+"
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel() defer cancel()
for { for {
err := Subscribe(ctx.Done(), topicFilter) err := Subscribe(ctx.Done(), "demo/+")
switch { switch {
case err == nil: case err == nil:
fmt.Printf("subscribed to %q", topicFilter) fmt.Println("subscribe confirmed by broker")
return return
case mqtt.IsDeny(err), errors.Is(err, mqtt.ErrClosed): case errors.As(err, new(mqtt.SubscribeError)):
fmt.Print("no subscribe: ", err) fmt.Println("subscribe failed by broker")
return return
case errors.Is(err, mqtt.ErrCanceled), errors.Is(err, mqtt.ErrAbandoned): case mqtt.IsDeny(err): // illegal topic filter
fmt.Print("subscribe timeout: ", err) fmt.Println(err)
return return
case errors.Is(err, mqtt.ErrClosed):
fmt.Println("no subscribe due client close")
return
case errors.Is(err, mqtt.ErrCanceled):
fmt.Println("no subscribe due timeout")
return
case errors.Is(err, mqtt.ErrAbandoned):
fmt.Println("subscribe state unknown due timeout")
return
case errors.Is(err, mqtt.ErrBreak):
fmt.Println("subscribe state unknown due connection loss")
select {
case <-Online():
fmt.Println("subscribe retry with new connection")
case <-ctx.Done():
fmt.Println("subscribe timeout")
return
}
case errors.Is(err, mqtt.ErrDown): case errors.Is(err, mqtt.ErrDown):
<-Online() fmt.Println("subscribe delay while service is down")
select {
case <-Online():
fmt.Println("subscribe retry with new connection")
case <-ctx.Done():
fmt.Println("subscribe timeout")
return
}
case errors.Is(err, mqtt.ErrMax): case errors.Is(err, mqtt.ErrMax): // limit is quite high
time.Sleep(time.Second / 2) fmt.Println("subscribe hold-up:", err)
time.Sleep(2 * time.Second) // backoff
default: default:
backoff := 4 * time.Second fmt.Println("subscribe request transfer interupted:", err)
fmt.Printf("subscribe retry in %s on: %s", backoff, err) time.Sleep(time.Second / 2) // backoff
time.Sleep(backoff)
} }
} }
// Output: // Output:
// subscribed to "demo/+" // subscribe confirmed by broker
} }

View File

@@ -95,51 +95,51 @@ func NewPublishStub(returnFix error) func(quit <-chan struct{}, message []byte,
} }
} }
// AckBlock prevents ack <-chan error submission. // ExchangeBlock prevents exchange <-chan error submission.
type AckBlock struct { type ExchangeBlock struct {
Delay time.Duration // zero defaults to indefinite Delay time.Duration // zero defaults to indefinite
} }
// Error implements the standard error interface. // Error implements the standard error interface.
func (b AckBlock) Error() string { func (b ExchangeBlock) Error() string {
return "mqtttest: AckBlock used as an error" return "mqtttest: ExchangeBlock used as an error"
} }
// NewPublishAckStub returns a stub for mqtt.Client PublishAtLeastOnce or // NewPublishEnqueuedStub returns a stub for mqtt.Client PublishAtLeastOnce or
// PublishExactlyOnce with a fixed return value. // PublishExactlyOnce with a fixed return value.
// //
// The ackFix errors are applied to the ack return, with an option for AckBlock // The exchangeFix errors are applied to the exchange return, with an option for
// entries. An mqtt.ErrClosed in the ackFix keeps the ack channel open (without // ExchangeBlock entries. An mqtt.ErrClosed in the exchangeFix keeps the
// an extra AckBlock entry. // exchange channel open (without an extra ExchangeBlock entry).
func NewPublishAckStub(errFix error, ackFix ...error) func(message []byte, topic string) (ack <-chan error, err error) { func NewPublishEnqueuedStub(errFix error, exchangeFix ...error) func(message []byte, topic string) (exchange <-chan error, err error) {
if errFix != nil && len(ackFix) != 0 { if errFix != nil && len(exchangeFix) != 0 {
panic("ackFix entries with non-nil errFix") panic("exchangeFix entries with non-nil errFix")
} }
var block AckBlock var block ExchangeBlock
for i, err := range ackFix { for i, err := range exchangeFix {
switch { switch {
case err == nil: case err == nil:
panic("nil entry in ackFix") panic("nil entry in exchangeFix")
case errors.Is(err, mqtt.ErrClosed): case errors.Is(err, mqtt.ErrClosed):
if i+1 < len(ackFix) { if i+1 < len(exchangeFix) {
panic("followup of mqtt.ErrClosed ackFix entry") panic("followup on mqtt.ErrClosed exchangeFix entry")
} }
case errors.As(err, &block): case errors.As(err, &block):
if block.Delay == 0 && i+1 < len(ackFix) { if block.Delay == 0 && i+1 < len(exchangeFix) {
panic("followup of indefinite AckBlock ackFix entry") panic("followup on indefinite ExchangeBlock exchangeFix entry")
} }
} }
} }
return func(message []byte, topic string) (ack <-chan error, err error) { return func(message []byte, topic string) (exchange <-chan error, err error) {
if errFix != nil { if errFix != nil {
return nil, errFix return nil, errFix
} }
ch := make(chan error, len(ackFix)) ch := make(chan error, len(exchangeFix))
go func() { go func() {
var block AckBlock var block ExchangeBlock
for _, err := range ackFix { for _, err := range exchangeFix {
switch { switch {
default: default:
ch <- err ch <- err

View File

@@ -9,12 +9,12 @@ import (
// Signatures // Signatures
var ( var (
client mqtt.Client client mqtt.Client
subscribe = client.Subscribe subscribe = client.Subscribe
unsubscribe = client.Unsubscribe unsubscribe = client.Unsubscribe
publish = client.Publish publish = client.Publish
publishAck = client.PublishAtLeastOnce publishEnqueued = client.PublishAtLeastOnce
readSlices = client.ReadSlices readSlices = client.ReadSlices
) )
// Won't compile on failure. // Won't compile on failure.
@@ -23,13 +23,13 @@ func TestSignatureMatch(t *testing.T) {
// check dupe assumptions // check dupe assumptions
subscribe = c.SubscribeLimitAtMostOnce subscribe = c.SubscribeLimitAtMostOnce
subscribe = c.SubscribeLimitAtLeastOnce subscribe = c.SubscribeLimitAtLeastOnce
publishAck = c.PublishExactlyOnce publishEnqueued = c.PublishExactlyOnce
// check fits // check fits
readSlices = mqtttest.NewReadSlicesMock(t) readSlices = mqtttest.NewReadSlicesMock(t)
publish = mqtttest.NewPublishMock(t) publish = mqtttest.NewPublishMock(t)
publish = mqtttest.NewPublishStub(nil) publish = mqtttest.NewPublishStub(nil)
publishAck = mqtttest.NewPublishAckStub(nil) publishEnqueued = mqtttest.NewPublishEnqueuedStub(nil)
subscribe = mqtttest.NewSubscribeMock(t) subscribe = mqtttest.NewSubscribeMock(t)
subscribe = mqtttest.NewSubscribeStub(nil) subscribe = mqtttest.NewSubscribeStub(nil)
unsubscribe = mqtttest.NewUnsubscribeMock(t) unsubscribe = mqtttest.NewUnsubscribeMock(t)

View File

@@ -20,9 +20,13 @@ var ErrMax = errors.New("mqtt: maximum number of pending requests reached")
var ErrCanceled = errors.New("mqtt: request canceled before submission") var ErrCanceled = errors.New("mqtt: request canceled before submission")
// ErrAbandoned means that a quit signal got applied after the request was send. // ErrAbandoned means that a quit signal got applied after the request was send.
// The result remains unknown, as opposed to ErrCanceled. // The broker received the request, yet the result/reponse remains unkown.
var ErrAbandoned = errors.New("mqtt: request abandoned after submission") var ErrAbandoned = errors.New("mqtt: request abandoned after submission")
// ErrBreak means that the connection broke up after the request was send.
// The broker received the request, yet the result/reponse remains unkown.
var ErrBreak = errors.New("mqtt: connection lost while awaiting response")
// BufSize should fit topic names with a bit of overhead. // BufSize should fit topic names with a bit of overhead.
const bufSize = 128 const bufSize = 128
@@ -187,12 +191,12 @@ func (txs *unorderedTxs) endTx(packetID uint16) (done chan<- error, topicFilters
return callback.done, callback.topicFilters return callback.done, callback.topicFilters
} }
func (txs *unorderedTxs) close() { func (txs *unorderedTxs) breakAll() {
txs.Lock() txs.Lock()
defer txs.Unlock() defer txs.Unlock()
for packetID, callback := range txs.perPacketID { for packetID, callback := range txs.perPacketID {
delete(txs.perPacketID, packetID) delete(txs.perPacketID, packetID)
callback.done <- ErrClosed callback.done <- ErrBreak
} }
} }
@@ -307,9 +311,7 @@ func (c *Client) onSUBACK() error {
return errProtoReset return errProtoReset
} }
if failN == 0 { if failN != 0 {
close(done)
} else {
var err SubscribeError var err SubscribeError
for i, code := range returnCodes { for i, code := range returnCodes {
if code == 0x80 { if code == 0x80 {
@@ -318,6 +320,7 @@ func (c *Client) onSUBACK() error {
} }
done <- err done <- err
} }
close(done)
return nil return nil
} }
@@ -412,7 +415,7 @@ type holdup struct {
// Publish delivers the message with an “at most once” guarantee. // Publish delivers the message with an “at most once” guarantee.
// Subscribers may or may not receive the message when subject to error. // Subscribers may or may not receive the message when subject to error.
// This delivery method is the most efficient option. // This delivery method is the most efficient option.
/// //
// Quit is optional, as nil just blocks. Appliance of quit will strictly result // Quit is optional, as nil just blocks. Appliance of quit will strictly result
// in ErrCanceled. // in ErrCanceled.
func (c *Client) Publish(quit <-chan struct{}, message []byte, topic string) error { func (c *Client) Publish(quit <-chan struct{}, message []byte, topic string) error {
@@ -445,10 +448,9 @@ func (c *Client) PublishRetained(quit <-chan struct{}, message []byte, topic str
// This delivery method requires a response transmission plus persistence on // This delivery method requires a response transmission plus persistence on
// both client-side and broker-side. // both client-side and broker-side.
// //
// The acknowledge channel is closed uppon receival confirmation by the broker. // The exchange channel is closed uppon receival confirmation by the broker.
// ErrClosed leaves the channel blocked (with no further input). A blocked send // ErrClosed leaves the channel blocked (with no further input).
// from ReadSlices causes the error to be returned instead. func (c *Client) PublishAtLeastOnce(message []byte, topic string) (exchange <-chan error, err error) {
func (c *Client) PublishAtLeastOnce(message []byte, topic string) (ack <-chan error, err error) {
buf := bufPool.Get().(*[bufSize]byte) buf := bufPool.Get().(*[bufSize]byte)
defer bufPool.Put(buf) defer bufPool.Put(buf)
packet, err := appendPublishPacket(buf, message, topic, atLeastOnceIDSpace, typePUBLISH<<4|atLeastOnceLevel<<1) packet, err := appendPublishPacket(buf, message, topic, atLeastOnceIDSpace, typePUBLISH<<4|atLeastOnceLevel<<1)
@@ -463,7 +465,7 @@ func (c *Client) PublishAtLeastOnce(message []byte, topic string) (ack <-chan er
// subscriptions match the topic name. When a new subscription is established, // subscriptions match the topic name. When a new subscription is established,
// the last retained message, if any, on each matching topic name must be sent // the last retained message, if any, on each matching topic name must be sent
// to the subscriber. // to the subscriber.
func (c *Client) PublishAtLeastOnceRetained(message []byte, topic string) (ack <-chan error, err error) { func (c *Client) PublishAtLeastOnceRetained(message []byte, topic string) (exchange <-chan error, err error) {
buf := bufPool.Get().(*[bufSize]byte) buf := bufPool.Get().(*[bufSize]byte)
defer bufPool.Put(buf) defer bufPool.Put(buf)
packet, err := appendPublishPacket(buf, message, topic, atLeastOnceIDSpace, typePUBLISH<<4|atLeastOnceLevel<<1|retainFlag) packet, err := appendPublishPacket(buf, message, topic, atLeastOnceIDSpace, typePUBLISH<<4|atLeastOnceLevel<<1|retainFlag)
@@ -476,7 +478,7 @@ func (c *Client) PublishAtLeastOnceRetained(message []byte, topic string) (ack <
// PublishExactlyOnce delivers the message with an “exactly once” guarantee. // PublishExactlyOnce delivers the message with an “exactly once” guarantee.
// This delivery method eliminates the duplicate-delivery risk from // This delivery method eliminates the duplicate-delivery risk from
// PublishAtLeastOnce at the expense of an additional network roundtrip. // PublishAtLeastOnce at the expense of an additional network roundtrip.
func (c *Client) PublishExactlyOnce(message []byte, topic string) (ack <-chan error, err error) { func (c *Client) PublishExactlyOnce(message []byte, topic string) (exchange <-chan error, err error) {
buf := bufPool.Get().(*[bufSize]byte) buf := bufPool.Get().(*[bufSize]byte)
defer bufPool.Put(buf) defer bufPool.Put(buf)
packet, err := appendPublishPacket(buf, message, topic, exactlyOnceIDSpace, typePUBLISH<<4|exactlyOnceLevel<<1) packet, err := appendPublishPacket(buf, message, topic, exactlyOnceIDSpace, typePUBLISH<<4|exactlyOnceLevel<<1)
@@ -491,7 +493,7 @@ func (c *Client) PublishExactlyOnce(message []byte, topic string) (ack <-chan er
// subscriptions match the topic name. When a new subscription is established, // subscriptions match the topic name. When a new subscription is established,
// the last retained message, if any, on each matching topic name must be sent // the last retained message, if any, on each matching topic name must be sent
// to the subscriber. // to the subscriber.
func (c *Client) PublishExactlyOnceRetained(message []byte, topic string) (ack <-chan error, err error) { func (c *Client) PublishExactlyOnceRetained(message []byte, topic string) (exchange <-chan error, err error) {
buf := bufPool.Get().(*[bufSize]byte) buf := bufPool.Get().(*[bufSize]byte)
defer bufPool.Put(buf) defer bufPool.Put(buf)
packet, err := appendPublishPacket(buf, message, topic, exactlyOnceIDSpace, typePUBLISH<<4|exactlyOnceLevel<<1|retainFlag) packet, err := appendPublishPacket(buf, message, topic, exactlyOnceIDSpace, typePUBLISH<<4|exactlyOnceLevel<<1|retainFlag)
@@ -501,8 +503,8 @@ func (c *Client) PublishExactlyOnceRetained(message []byte, topic string) (ack <
return c.submitPersisted(packet, c.exactlyOnceSem, c.recQ, c.compQ, c.exactlyOnceBlock) return c.submitPersisted(packet, c.exactlyOnceSem, c.recQ, c.compQ, c.exactlyOnceBlock)
} }
func (c *Client) submitPersisted(packet net.Buffers, sem chan uint, ackQ, ackQ2 chan chan<- error, block chan holdup) (ack <-chan error, err error) { func (c *Client) submitPersisted(packet net.Buffers, sem chan uint, ackQ, ackQ2 chan chan<- error, block chan holdup) (exchange <-chan error, err error) {
done := make(chan error, 2) done := make(chan error, 2) // receives at most 1 write error + ErrClosed
select { select {
case counter, ok := <-sem: case counter, ok := <-sem:
if !ok { if !ok {
@@ -519,12 +521,11 @@ func (c *Client) submitPersisted(packet net.Buffers, sem chan uint, ackQ, ackQ2
return nil, err return nil, err
} }
ackQ <- done // won't block due ErrMax check ackQ <- done // won't block due ErrMax check
switch err := c.writeAll(c.Offline(), packet); { switch err := c.writeAll(c.Offline(), packet); {
case err == nil: case err == nil:
sem <- counter + 1 sem <- counter + 1
case errors.Is(err, ErrCanceled): case errors.Is(err, ErrCanceled), errors.Is(err, ErrDown):
done <- ErrDown // don't report down
block <- holdup{SinceSeqNo: counter, UntilSeqNo: counter} block <- holdup{SinceSeqNo: counter, UntilSeqNo: counter}
default: default:
done <- err done <- err
@@ -545,7 +546,6 @@ func (c *Client) submitPersisted(packet net.Buffers, sem chan uint, ackQ, ackQ2
ackQ <- done // won't block due ErrMax check ackQ <- done // won't block due ErrMax check
holdup.UntilSeqNo++ holdup.UntilSeqNo++
block <- holdup block <- holdup
done <- ErrDown
} }
return done, nil return done, nil
@@ -553,14 +553,14 @@ func (c *Client) submitPersisted(packet net.Buffers, sem chan uint, ackQ, ackQ2
func appendPublishPacket(buf *[bufSize]byte, message []byte, topic string, packetID uint, head byte) (net.Buffers, error) { func appendPublishPacket(buf *[bufSize]byte, message []byte, topic string, packetID uint, head byte) (net.Buffers, error) {
if err := stringCheck(topic); err != nil { if err := stringCheck(topic); err != nil {
return nil, fmt.Errorf("mqtt: PUBLISH denied due topic: %w", err) return nil, fmt.Errorf("mqtt: PUBLISH request denied due topic: %w", err)
} }
size := 2 + len(topic) + len(message) size := 2 + len(topic) + len(message)
if packetID != 0 { if packetID != 0 {
size += 2 size += 2
} }
if size < 0 || size > packetMax { if size < 0 || size > packetMax {
return nil, fmt.Errorf("mqtt: PUBLISH denied: %w", errPacketMax) return nil, fmt.Errorf("mqtt: PUBLISH request denied: %w", errPacketMax)
} }
packet := append(buf[:0], head) packet := append(buf[:0], head)