Skip to content

Commit cb97a80

Browse files
author
Ahmad Faiz Kamaludin
committed
add support for router context based
1 parent 1aa0396 commit cb97a80

7 files changed

Lines changed: 459 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
RouterContext Example
2+
===
3+
4+
AutoPaho provides the option to specify callbacks (`ClientConfig.OnPublishReceived`) that will be called everytime
5+
a message is received. It's fairly common for users to want multiple callbacks with the message topic determining which
6+
callback is called. Routers can provide this functionality.
7+
8+
To use them first create a router:
9+
10+
```
11+
router := paho.NewStandardContextRouter()
12+
```
13+
14+
Configure `ClientConfig.OnPublishReceived` so the router is called:
15+
16+
```go
17+
autopaho.ClientConfig{
18+
OnPublishReceived: []func (paho.PublishReceived) (bool, error){
19+
func (pr paho.PublishReceived) (bool, error) {
20+
router.Route(pr.Packet.Packet())
21+
return true, nil // we assume that the router handles all messages (todo: amend router API)
22+
}},
23+
}
24+
```
25+
26+
Now you can add/remove routes:
27+
28+
```go
29+
router.DefaultHandler(func(ctx context.Context, p *paho.Publish) {
30+
slog.InfoContext(ctx, fmt.Sprintf("defaulthandler received message with topic: %s", p.Topic))
31+
})
32+
router.RegisterHandler("test/test/#", func(ctx context.Context, p *paho.Publish) {
33+
slog.InfoContext(ctx, fmt.Sprintf("test/test/# received message with topic: %s", p.Topic))
34+
})
35+
router.UnregisterHandler("test/test/#")
36+
```
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright (c) 2024 Contributors to the Eclipse Foundation
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License v2.0
6+
* and Eclipse Distribution License v1.0 which accompany this distribution.
7+
*
8+
* The Eclipse Public License is available at
9+
* https://www.eclipse.org/legal/epl-2.0/
10+
* and the Eclipse Distribution License is available at
11+
* http://www.eclipse.org/org/documents/edl-v10.php.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
14+
*/
15+
16+
package main
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"log/slog"
22+
"net/url"
23+
"os"
24+
"os/signal"
25+
"syscall"
26+
27+
"github.com/eclipse/paho.golang/autopaho"
28+
"github.com/eclipse/paho.golang/autopaho/examples/routercontext/middleware"
29+
"github.com/eclipse/paho.golang/paho"
30+
)
31+
32+
const clientID = "PahoGoClient" // Change this to something random if using a public test server
33+
34+
// This example demonstrates the use of a StandardRouter; please note that the router API is likely to change
35+
// prior to the release of v1.0.
36+
37+
func main() {
38+
// App will run until cancelled by user (e.g. ctrl-c)
39+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
40+
defer stop()
41+
42+
// We will connect to the Mosquitto test server (note that you may see messages that other users publish)
43+
u, err := url.Parse("mqtt://test.mosquitto.org:1883")
44+
if err != nil {
45+
panic(err)
46+
}
47+
48+
router := paho.NewStandardContextRouter()
49+
50+
// Registering middleware handlers
51+
router.Use(middleware.Logger)
52+
router.Use(middleware.Recoverer)
53+
54+
router.DefaultHandler(func(ctx context.Context, p *paho.Publish) {
55+
slog.InfoContext(ctx, fmt.Sprintf("defaulthandler received message with topic: %s", p.Topic))
56+
})
57+
58+
cliCfg := autopaho.ClientConfig{
59+
ServerUrls: []*url.URL{u},
60+
KeepAlive: 20, // Keepalive message should be sent every 20 seconds
61+
// We don't want the broker to delete any session info when we disconnect
62+
CleanStartOnInitialConnection: true,
63+
SessionExpiryInterval: 0,
64+
OnConnectError: func(err error) { fmt.Printf("error whilst attempting connection: %s\n", err) },
65+
// eclipse/paho.golang/paho provides base mqtt functionality, the below config will be passed in for each connection
66+
ClientConfig: paho.ClientConfig{
67+
// If you are using QOS 1/2, then it's important to specify a client id (which must be unique)
68+
ClientID: clientID,
69+
// OnPublishReceived is a slice of functions that will be called when a message is received.
70+
// You can write the function(s) yourself or use the supplied Router
71+
OnPublishReceived: []func(paho.PublishReceived) (bool, error){
72+
func(pr paho.PublishReceived) (bool, error) {
73+
router.Route(pr.Packet.Packet())
74+
return true, nil // we assume that the router handles all messages (todo: amend router API)
75+
}},
76+
OnClientError: func(err error) { fmt.Printf("client error: %s\n", err) },
77+
OnServerDisconnect: func(d *paho.Disconnect) {
78+
if d.Properties != nil {
79+
fmt.Printf("server requested disconnect: %s\n", d.Properties.ReasonString)
80+
} else {
81+
fmt.Printf("server requested disconnect; reason code: %d\n", d.ReasonCode)
82+
}
83+
},
84+
},
85+
}
86+
87+
c, err := autopaho.NewConnection(ctx, cliCfg) // starts process; will reconnect until context cancelled
88+
if err != nil {
89+
panic(err)
90+
}
91+
92+
if err = c.AwaitConnection(ctx); err != nil {
93+
panic(err)
94+
}
95+
96+
// In most cases subscribing in OnConnectionUp is recommended (so subscription will be re-established after
97+
// a reconnection. However, for the purposes of this demo subscribing here is simpler.
98+
if _, err := c.Subscribe(context.Background(), &paho.Subscribe{
99+
Subscriptions: []paho.SubscribeOptions{
100+
{Topic: "test/#", QoS: 1}, // For this example, we get all messages under test
101+
},
102+
}); err != nil {
103+
panic(fmt.Sprintf("failed to subscribe (%s). This is likely to mean no messages will be received.", err))
104+
}
105+
106+
// Handlers can be registered/deregistered at any time. It's important to note that you need to subscribe AND create
107+
// a handler
108+
router.RegisterHandler("test/test/#", func(ctx context.Context, p *paho.Publish) {
109+
slog.InfoContext(ctx, fmt.Sprintf("test/test/# received message with topic: %s", p.Topic))
110+
})
111+
router.RegisterHandler("test/test/foo", func(ctx context.Context, p *paho.Publish) {
112+
slog.InfoContext(ctx, fmt.Sprintf("test/test/foo received message with topic: %s", p.Topic))
113+
})
114+
router.RegisterHandler("test/nomatch", func(ctx context.Context, p *paho.Publish) {
115+
slog.InfoContext(ctx, fmt.Sprintf("test/nomatch received message with topic: %s", p.Topic))
116+
})
117+
router.RegisterHandler("test/panic", func(ctx context.Context, p *paho.Publish) {
118+
slog.InfoContext(ctx, fmt.Sprintf("test/panic received message with topic: %s", p.Topic))
119+
panic("This is a panic!")
120+
})
121+
router.RegisterHandler("test/quit", func(_ context.Context, p *paho.Publish) { stop() }) // Context will be cancelled if we receive a matching message
122+
123+
// We publish three messages to test out the various route handlers
124+
topics := []string{"test/test", "test/test/foo", "test/xxNoMatch", "test/panic", "test/quit"}
125+
for _, t := range topics {
126+
if _, err := c.Publish(ctx, &paho.Publish{
127+
QoS: 1,
128+
Topic: t,
129+
Payload: []byte("TestMessage on topic: " + t),
130+
}); err != nil {
131+
if ctx.Err() == nil {
132+
panic(err) // Publish will exit when context cancelled or if something went wrong
133+
}
134+
}
135+
}
136+
137+
<-c.Done() // Wait for clean shutdown (cancelling the context triggered the shutdown)
138+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package middleware
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"time"
7+
8+
"github.com/eclipse/paho.golang/paho"
9+
)
10+
11+
func Logger(next paho.MessageContextHandler) paho.MessageContextHandler {
12+
return func(ctx context.Context, p *paho.Publish) {
13+
start := time.Now()
14+
next(ctx, p)
15+
16+
elapsed := time.Since(start)
17+
slog.InfoContext(ctx, "message procesed",
18+
slog.String("topic", p.Topic),
19+
slog.Int("packet_id", int(p.PacketID)),
20+
slog.Int("qos", int(p.QoS)),
21+
slog.Duration("latency", elapsed),
22+
)
23+
}
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package middleware
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"runtime/debug"
7+
8+
"github.com/eclipse/paho.golang/paho"
9+
)
10+
11+
func Recoverer(next paho.MessageContextHandler) paho.MessageContextHandler {
12+
return func(ctx context.Context, p *paho.Publish) {
13+
defer func() {
14+
if r := recover(); r != nil {
15+
fmt.Println("Recovered in f", r)
16+
debug.PrintStack()
17+
}
18+
}()
19+
20+
next(ctx, p)
21+
}
22+
}

paho/client.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ type (
7575
// slice (which provides a more flexible approach to handling incoming messages).
7676
Router Router
7777

78+
// ContextRouter is an alternative message router that receives context.Context along with the message.
79+
// It is used when Router is not set, allowing context-aware message handling for operations
80+
// that require context propagation, cancellation, or timeout control.
81+
ContextRouter ContextRouter
82+
7883
// OnPublishReceived provides a slice of callbacks; additional handlers may be added after the client has been
7984
// created via the AddOnPublishReceived function (Client holds a copy of the slice; OnPublishReceived will not change).
8085
// When a `PUBLISH` is received, the callbacks will be called in order. If a callback processes the message,
@@ -197,6 +202,13 @@ func NewClient(conf ClientConfig) *Client {
197202
r.Route(p.Packet.Packet())
198203
return false, nil
199204
})
205+
} else if c.config.ContextRouter != nil {
206+
r := c.config.ContextRouter
207+
c.onPublishReceived = append(c.onPublishReceived,
208+
func(p PublishReceived) (bool, error) {
209+
r.Route(p.Packet.Packet())
210+
return false, nil
211+
})
200212
}
201213
c.onPublishReceivedTracker = make([]int, len(c.onPublishReceived)) // Must have the same number of elements as onPublishReceived
202214

0 commit comments

Comments
 (0)