Files
mqtt-5-learning/lessons/02-go-connection.md
T

261 lines
14 KiB
Markdown

# 🔌 Section 2 — Understand a Go MQTT 5 connection
Prerequisite: Section 1's broker and backend credentials work. This section contains a complete small program. Type it in stages, compile after each meaningful change, and explain the code before running the failure exercises.
**Version baseline:** Paho `v0.23.0`, verified from upstream release tags on 2026-10-07. It requires Go 1.24 or newer; Go 1.25 is compatible. The matching local reference is `references/paho.golang-v0.23.0/` (commit `1aa03969c5932464359b3e4ed752d9ac61abccc9`).
## 🎬 Start with the system story
> 💡 **Story question:** The broker is running and has accepted CLI clients. How does the Go backend become another MQTT client, and what event proves the broker accepted its connection?
The Go process loads its connection values, gives AutoPaho a client configuration, and starts a connection manager. AutoPaho manages the connection lifecycle; the underlying Paho client performs MQTT 5 protocol exchanges. The manager can start before the network connection succeeds, so the application waits for a successful CONNACK through `AwaitConnection`.
```mermaid
sequenceDiagram
participant App as Go backend application
participant Auto as AutoPaho manager
participant Paho as Paho MQTT 5 client
participant Broker as Mosquitto broker
App->>Auto: NewConnection(lifecycle context, config)
Auto->>Paho: create and manage client connection
Paho->>Broker: TCP connection, then CONNECT
Broker->>Broker: authenticate client credentials
Broker-->>Paho: CONNACK accepted or rejected
Paho-->>Auto: connection state / callback
App->>Auto: AwaitConnection(startup deadline)
Auto-->>App: connected or startup error
```
The backend is an MQTT client, just like the device. Its application role does not make the broker handshake different. A successful CONNACK proves connection acceptance; subscriptions, device availability, and business completion are later evidence. See [Lesson 0](00-system-lifecycle.md) for the complete lifecycle.
## 📁 Working directory and file placement
Run every shell command in this lesson from the learning project root:
```sh
cd /home/ryan/Media/Code/Code/robot-market/mqtt-5-learning
```
Paths such as `cmd/backend/main.go` are relative to this directory. Markdown files under `lessons/` explain the work; create the application files at the paths shown, not inside `lessons/` or `references/`. Code blocks labeled Go, TOML, YAML, or conf are file contents; shell blocks are terminal commands. Create parent directories before saving files.
## 🔌 1. Build the connection program
> 💡 **Story question:** Which settings and lifecycle controls must the Go process pass to the client library so a connection attempt can start and stop predictably?
The program builds one AutoPaho configuration from the broker URL, credentials, Client ID, connection options, and callbacks. The root context owns the manager lifetime; a separate startup context bounds how long the application waits for the first connection.
Create the directory first:
```sh
mkdir -p cmd/backend
```
Create `cmd/backend/main.go` and paste the complete program below into it. If the file already exists, replace its contents rather than appending a second program:
```go
package main
import (
"context"
"fmt"
"log"
"net/url"
"os"
"os/signal"
"syscall"
"time"
"github.com/eclipse/paho.golang/autopaho"
"github.com/eclipse/paho.golang/paho"
)
func main() {
if err := run(); err != nil {
log.Print(err)
os.Exit(1)
}
}
func run() error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
user := os.Getenv("TEST_MQTT5_MQTT_USERNAME")
password := os.Getenv("TEST_MQTT5_MQTT_PASSWORD")
if user == "" || password == "" {
return fmt.Errorf("set the lab username and password environment variables")
}
broker, err := url.Parse("mqtt://127.0.0.1:18883")
if err != nil {
return fmt.Errorf("parse broker URL: %w", err)
}
cfg := autopaho.ClientConfig{
ServerUrls: []*url.URL{broker},
KeepAlive: 20,
CleanStartOnInitialConnection: true,
SessionExpiryInterval: 0,
ConnectUsername: user,
ConnectPassword: []byte(password),
OnConnectionUp: func(_ *autopaho.ConnectionManager, ack *paho.Connack) {
log.Printf("connected: session_present=%t reason=0x%02x", ack.SessionPresent, ack.ReasonCode)
},
OnConnectionDown: func() bool {
log.Print("connection down; AutoPaho will retry")
return true
},
OnConnectError: func(err error) {
// Keep errors free of credentials; never log cfg or broker userinfo.
log.Printf("connect attempt failed: %v", err)
},
ClientConfig: paho.ClientConfig{
ClientID: "lab-backend",
OnClientError: func(err error) { log.Printf("client error: %v", err) },
OnServerDisconnect: func(packet *paho.Disconnect) {
log.Printf("server disconnect: reason=0x%02x", packet.ReasonCode)
},
},
}
manager, err := autopaho.NewConnection(ctx, cfg)
if err != nil {
return fmt.Errorf("create connection manager: %w", err)
}
defer func() {
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := manager.Disconnect(shutdown); err != nil {
log.Printf("shutdown did not finish within deadline: %v", err)
}
}()
startup, cancel := context.WithTimeout(ctx, 10*time.Second)
err = manager.AwaitConnection(startup)
cancel()
if err != nil {
return fmt.Errorf("initial connection: %w", err)
}
log.Print("connection ready; press Ctrl+C to stop")
<-ctx.Done()
return nil
}
```
## 🔌 2. Understand the connection lifecycle
> 💡 **Story question:** What is happening between creating the manager and logging “connection ready,” and why are those moments not equivalent?
Trace the background attempt through transport connection, MQTT CONNECT, broker authentication, CONNACK, and `AwaitConnection`. Then trace what AutoPaho does when the connection drops.
Before reading the full program, predict what evidence you need at each stage:
```text
Go process → AutoPaho retry loop → TCP connection → CONNECT → CONNACK
→ subscriptions (Lesson 4)
```
AutoPaho owns reconnect attempts and creates the underlying Paho client for a connection. Paho implements MQTT packets and protocol exchanges. Your application owns readiness, authorization policy, command state, and business retries. A running goroutine or open TCP socket is too early to declare MQTT ready: the broker can still reject CONNECT.
| Setting or call | MQTT meaning and reason for this choice |
| --- | --- |
| `ServerUrls` | Where to dial; the lab host endpoint is different from the Compose service endpoint |
| `ClientID` | Broker session identity; two simultaneous processes with this ID compete for one connection |
| `ConnectUsername`, `ConnectPassword` | CONNECT authentication inputs; independent of Client ID and payload identity |
| `KeepAlive: 20` | Maximum idle interval between client control packets, in seconds; Paho handles PINGREQ/PINGRESP when needed |
| `CleanStartOnInitialConnection: true` | Discard a previous broker session on the manager's first successful connection |
| `SessionExpiryInterval: 0` | Do not retain this broker session after the connection closes |
| `NewConnection` | Start connection management; a nil error does not establish a successful CONNACK |
| `AwaitConnection` | Wait for connection availability; it does not wait for SUBACK or a device heartbeat |
| `OnConnectionUp` | Observe a successful connection, including reconnection; inspect CONNACK evidence here |
| `OnConnectError` | Observe failed attempts, including refused connections or unsuccessful CONNACK |
| `OnConnectionDown` | Decide whether reconnect attempts should continue; returning false ends that retry lifecycle |
| `OnServerDisconnect` | Inspect a DISCONNECT actually sent by the broker; a severed cable cannot send this packet |
Keep Alive checks the MQTT connection, not the health of the vending application. With an effective Keep Alive of 20 seconds, the broker's protocol inactivity limit is 30 seconds without a client control packet; an already-detected closed socket can be handled earlier. A CONNACK Server Keep Alive can override the requested value. Your three-second heartbeat in Lesson 4 carries application evidence and has a separate purpose.
There are three different contexts here. The signal context controls the program lifetime. `startup` limits the initial wait to ten seconds. `shutdown` is a fresh context because the signal context is already canceled during shutdown. The manager receives the long-lived context, not the startup deadline; passing the startup context to `NewConnection` would terminate your service when that deadline expires.
`CleanStartOnInitialConnection: true` and `SessionExpiryInterval: 0` choose a disposable early-lab session. This is convenient for first experiments and intentionally offers no offline broker session after disconnect. Later lessons change these settings and test recovery. Do not copy them as a durable production policy.
Paho v0.23.0's `Disconnect(ctx)` cancels the manager and waits for manager shutdown with the provided deadline. Its `Done()` documentation notes that underlying MQTT cleanup may still be completing; neither API is proof that application data was persisted or device work was finished.
## ▶️ 3. Run it
> 💡 **Story question:** Which output tells you that the broker accepted the client, and what does a clean Ctrl+C demonstrate about lifecycle ownership?
Run the process with valid lab credentials, observe the acceptance evidence, then stop it and compare graceful disconnect with a process that simply disappears.
The backend reads its username and password from `configs/lab.toml`:
```sh
go run ./cmd/backend
```
`go run ./cmd/backend` compiles the package and runs it in the foreground. `./` means relative to the project root. Ctrl+C sends an interrupt to the foreground program; `signal.NotifyContext` turns it into context cancellation, allowing your cleanup code to run.
The TOML file contains the disposable local lab password. Use only lab credentials. Expect `connected` and `connection ready`; callback log order can vary. Press Ctrl+C and verify exit within the shutdown deadline.
Run syntax checks after creating the file:
```sh
gofmt -w cmd/backend/main.go
go build ./cmd/backend
go vet ./cmd/backend
```
`gofmt -w` rewrites the named source file using Go formatting. `go build` checks compilation and linking; `go vet` checks suspicious code patterns without running the application. A successful vet run is normally quiet.
`go build` emits a binary in the current directory; remove or ignore it as appropriate. A build verifies syntax/types, not connection behavior.
## ⚠️ 4. Observe the protocol and failures
> 💡 **Story question:** How can you tell whether a failure happened before MQTT, during broker authentication, or after an established connection was lost?
Change one condition at a time and compare the connection callbacks, reason information, retry behavior, and shutdown outcome.
CONNECT carries your client ID, authentication fields, keepalive, and session properties. A successful CONNACK is connection evidence. Bad credentials produce an unsuccessful connection attempt; the rejected CONNACK is handled by the connection-error path rather than your success callback.
1. Stop the broker before running the program. Expect retry diagnostics and initial-wait failure near ten seconds, followed by cleanup.
2. Start the broker; run the program; stop and restart the broker. Expect connection-down and connection-up events. You have not restored subscriptions yet because this program has none.
3. Supply a wrong password. Explain why retrying cannot repair an incorrect credential.
4. Run two copies using the same client ID. Observe connection takeover/reconnect behavior, then stop both. Unique IDs are necessary for concurrently running clients.
5. Press Ctrl+C while the broker is unavailable. Cancellation should interrupt the wait; do not wait for the full startup deadline.
## 🔎 Cautions and troubleshooting
- AutoPaho handles connection management; it does not invent topic authorization, business retries, or durable command storage.
- Do not call `log.Fatal` from callbacks. It terminates the process without running deferred cleanup.
- Callbacks should be short. Never add an indefinite channel send, database operation, or sleep to `OnConnectionDown`.
- Keep client IDs stable when you later want session resumption, but distinct across different simultaneously running clients.
- Logging entire config structs or URL userinfo can reveal passwords. These examples keep credentials separate from the URL.
- A connection-up event is not subscription readiness, and neither is device readiness.
**Completion:** Explain every field in `cfg`, distinguish the three contexts, and demonstrate successful startup, failed startup, reconnect, and cancellation.
## ✅ Result — the story so far
The backend now joins the broker as an authenticated MQTT 5 client. AutoPaho starts and manages the connection; the application waits until CONNACK confirms the broker accepted it, then keeps running and retries after a dropped connection. The backend has no subscriptions yet, so it is connected but is not listening for device messages.
## ❓ Questions to answer before Section 3
1. Why can `NewConnection` return before a usable broker connection exists?
2. What would happen if you passed `startup` to `NewConnection`?
3. Why does shutdown use `context.Background()` as its parent?
4. What does returning `false` from `OnConnectionDown` change?
<details>
<summary>Check your reasoning after answering</summary>
`NewConnection` starts a background lifecycle; CONNACK is later evidence. A startup context passed as the manager lifetime would stop the manager when that context expires or is canceled. Shutdown needs a fresh bounded context because the signal context is already canceled. Returning false from `OnConnectionDown` stops further automatic reconnect attempts for that manager; it does not repair the connection or preserve application state.
</details>
## 🔗 Sources
- [AutoPaho API v0.23.0](https://pkg.go.dev/github.com/eclipse/paho.golang@v0.23.0/autopaho)
- [Tagged connection manager source](https://github.com/eclipse-paho/paho.golang/blob/v0.23.0/autopaho/auto.go)
Next: [Section 3](03-typed-configuration.md).