first commit

This commit is contained in:
2026-10-08 13:45:52 +03:30
commit b234bd9108
31 changed files with 3034 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
# 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`).
## 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
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
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
In zsh, read your backend password without echoing it:
```sh
export TEST_MQTT5_MQTT_USERNAME=backend
read -rs 'TEST_MQTT5_MQTT_PASSWORD?Backend lab password: '
export TEST_MQTT5_MQTT_PASSWORD
go run ./cmd/backend
```
`export NAME=value` puts a variable into this terminal's environment so child processes such as `go run` inherit it. The `read` line uses zsh syntax: `-r` preserves backslashes, `-s` hides typed characters, and `NAME?prompt` selects the variable and prompt. Enter the password you created in Lesson 1; this does not create a broker user. The second `export` exposes that newly read variable to the Go process. In bash the prompt syntax differs; do not copy this zsh-specific line unchanged.
`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. After the process exits, `unset TEST_MQTT5_MQTT_PASSWORD` removes the credential from this shell's environment; it does not rotate the broker credential.
Environment variables avoid password literals in this command but are not a secrets vault. 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
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.
## 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).