get.pme.sh/pnats@v0.0.0-20240304004023-26bb5a137ed0/server/mqtt_ex_test.go (about) 1 // Copyright 2024 The NATS Authors 2 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // you may not use this file except in compliance with the License. 4 // You may obtain a copy of the License at 5 // 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 //go:build !skip_mqtt_tests 15 // +build !skip_mqtt_tests 16 17 package server 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "os" 24 "os/exec" 25 "strconv" 26 "testing" 27 "time" 28 ) 29 30 func TestMQTTExCompliance(t *testing.T) { 31 mqttPath := os.Getenv("MQTT_CLI") 32 if mqttPath == "" { 33 if p, err := exec.LookPath("mqtt"); err == nil { 34 mqttPath = p 35 } 36 } 37 if mqttPath == "" { 38 t.Skip(`"mqtt" command is not found in $PATH nor $MQTT_CLI. See https://hivemq.github.io/mqtt-cli/docs/installation/#debian-package for installation instructions`) 39 } 40 41 conf := createConfFile(t, []byte(fmt.Sprintf(` 42 listen: 127.0.0.1:-1 43 server_name: mqtt 44 jetstream { 45 store_dir = %q 46 } 47 mqtt { 48 listen: 127.0.0.1:-1 49 } 50 `, t.TempDir()))) 51 s, o := RunServerWithConfig(conf) 52 defer testMQTTShutdownServer(s) 53 54 cmd := exec.Command(mqttPath, "test", "-V", "3", "-p", strconv.Itoa(o.MQTT.Port)) 55 56 output, err := cmd.CombinedOutput() 57 t.Log(string(output)) 58 if err != nil { 59 if exitError, ok := err.(*exec.ExitError); ok { 60 t.Fatalf("mqtt cli exited with error: %v", exitError) 61 } 62 } 63 } 64 65 const ( 66 KB = 1024 67 ) 68 69 type mqttBenchMatrix struct { 70 QOS []int 71 MessageSize []int 72 Topics []int 73 Publishers []int 74 Subscribers []int 75 } 76 77 type mqttBenchContext struct { 78 QOS int 79 MessageSize int 80 Topics int 81 Publishers int 82 Subscribers int 83 84 Host string 85 Port int 86 87 // full path to mqtt-test command 88 testCmdPath string 89 } 90 91 var mqttBenchDefaultMatrix = mqttBenchMatrix{ 92 QOS: []int{0, 1, 2}, 93 MessageSize: []int{100, 1 * KB, 10 * KB}, 94 Topics: []int{100}, 95 Publishers: []int{1}, 96 Subscribers: []int{1}, 97 } 98 99 type MQTTBenchmarkResult struct { 100 Ops int `json:"ops"` 101 NS map[string]time.Duration `json:"ns"` 102 Bytes int64 `json:"bytes"` 103 } 104 105 func BenchmarkMQTTEx(b *testing.B) { 106 bc := mqttNewBenchEx(b) 107 b.Run("Server", func(b *testing.B) { 108 b.Cleanup(bc.startServer(b, false)) 109 bc.runAll(b) 110 }) 111 112 b.Run("Cluster", func(b *testing.B) { 113 b.Cleanup(bc.startCluster(b, false)) 114 bc.runAll(b) 115 }) 116 117 b.Run("Server-no-RMSCache", func(b *testing.B) { 118 b.Cleanup(bc.startServer(b, true)) 119 120 bc.benchmarkSubRet(b) 121 }) 122 123 b.Run("Cluster-no-RMSCache", func(b *testing.B) { 124 b.Cleanup(bc.startCluster(b, true)) 125 126 bc.benchmarkSubRet(b) 127 }) 128 } 129 130 func (bc mqttBenchContext) runAll(b *testing.B) { 131 bc.benchmarkPub(b) 132 bc.benchmarkPubRetained(b) 133 bc.benchmarkPubSub(b) 134 bc.benchmarkSubRet(b) 135 } 136 137 // makes a copy of bc 138 func (bc mqttBenchContext) benchmarkPub(b *testing.B) { 139 m := mqttBenchDefaultMatrix. 140 NoSubscribers(). 141 NoTopics() 142 143 b.Run("PUB", func(b *testing.B) { 144 m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) { 145 bc.runCommand(b, "pub", 146 "--qos", strconv.Itoa(bc.QOS), 147 "--n", strconv.Itoa(b.N), 148 "--size", strconv.Itoa(bc.MessageSize), 149 "--num-publishers", strconv.Itoa(bc.Publishers), 150 ) 151 }) 152 }) 153 } 154 155 // makes a copy of bc 156 func (bc mqttBenchContext) benchmarkPubRetained(b *testing.B) { 157 // This bench is meaningless for QOS0 since the client considers the message 158 // sent as soon as it's written out. It is also useless for QOS2 since the 159 // flow takes a lot longer, and the difference of publishing as retained or 160 // not is lost in the noise. 161 m := mqttBenchDefaultMatrix. 162 NoSubscribers(). 163 NoTopics(). 164 QOS1Only() 165 166 b.Run("PUBRET", func(b *testing.B) { 167 m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) { 168 bc.runCommand(b, "pub", "--retain", 169 "--qos", strconv.Itoa(bc.QOS), 170 "--n", strconv.Itoa(b.N), 171 "--size", strconv.Itoa(bc.MessageSize), 172 "--num-publishers", strconv.Itoa(bc.Publishers), 173 ) 174 }) 175 }) 176 } 177 178 // makes a copy of bc 179 func (bc mqttBenchContext) benchmarkPubSub(b *testing.B) { 180 // This test uses a single built-in topic, and a built-in publisher, so no 181 // reason to run it for topics and publishers. 182 m := mqttBenchDefaultMatrix. 183 NoTopics(). 184 NoPublishers() 185 186 b.Run("PUBSUB", func(b *testing.B) { 187 m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) { 188 bc.runCommand(b, "pubsub", 189 "--qos", strconv.Itoa(bc.QOS), 190 "--n", strconv.Itoa(b.N), 191 "--size", strconv.Itoa(bc.MessageSize), 192 "--num-subscribers", strconv.Itoa(bc.Subscribers), 193 ) 194 }) 195 }) 196 } 197 198 // makes a copy of bc 199 func (bc mqttBenchContext) benchmarkSubRet(b *testing.B) { 200 // This test uses a a built-in publisher, and it makes most sense to measure 201 // the retained message delivery "overhead" on a QoS0 subscription; without 202 // the extra time involved in actually subscribing. 203 m := mqttBenchDefaultMatrix. 204 NoPublishers(). 205 QOS0Only() 206 207 b.Run("SUBRET", func(b *testing.B) { 208 m.runMatrix(b, bc, func(b *testing.B, bc *mqttBenchContext) { 209 bc.runCommand(b, "subret", 210 "--qos", strconv.Itoa(bc.QOS), 211 "--n", strconv.Itoa(b.N), // number of subscribe requests 212 "--num-subscribers", strconv.Itoa(bc.Subscribers), 213 "--num-topics", strconv.Itoa(bc.Topics), 214 "--size", strconv.Itoa(bc.MessageSize), 215 ) 216 }) 217 }) 218 } 219 220 func mqttBenchLookupCommand(b *testing.B, name string) string { 221 b.Helper() 222 cmd, err := exec.LookPath(name) 223 if err != nil { 224 b.Skipf("%q command is not found in $PATH. Please `go install github.com/nats-io/meta-nats/apps/go/mqtt/...@latest` and try again.", name) 225 } 226 return cmd 227 } 228 229 func (bc mqttBenchContext) runCommand(b *testing.B, name string, extraArgs ...string) { 230 b.Helper() 231 232 args := append([]string{ 233 name, 234 "-q", 235 "--servers", fmt.Sprintf("%s:%d", bc.Host, bc.Port), 236 }, extraArgs...) 237 238 cmd := exec.Command(bc.testCmdPath, args...) 239 stdout, err := cmd.StdoutPipe() 240 if err != nil { 241 b.Fatalf("Error executing %q: %v", cmd.String(), err) 242 } 243 defer stdout.Close() 244 errbuf := bytes.Buffer{} 245 cmd.Stderr = &errbuf 246 if err = cmd.Start(); err != nil { 247 b.Fatalf("Error executing %q: %v", cmd.String(), err) 248 } 249 r := &MQTTBenchmarkResult{} 250 if err = json.NewDecoder(stdout).Decode(r); err != nil { 251 b.Fatalf("failed to decode output of %q: %v\n\n%s", cmd.String(), err, errbuf.String()) 252 } 253 if err = cmd.Wait(); err != nil { 254 b.Fatalf("Error executing %q: %v\n\n%s", cmd.String(), err, errbuf.String()) 255 } 256 257 r.report(b) 258 } 259 260 func (bc mqttBenchContext) initServer(b *testing.B) { 261 b.Helper() 262 bc.runCommand(b, "pubsub", 263 "--id", "__init__", 264 "--qos", "0", 265 "--n", "1", 266 "--size", "100", 267 "--num-subscribers", "1") 268 } 269 270 func (bc *mqttBenchContext) startServer(b *testing.B, disableRMSCache bool) func() { 271 b.Helper() 272 b.StopTimer() 273 prevDisableRMSCache := testDisableRMSCache 274 testDisableRMSCache = disableRMSCache 275 o := testMQTTDefaultOptions() 276 s := testMQTTRunServer(b, o) 277 278 o = s.getOpts() 279 bc.Host = o.MQTT.Host 280 bc.Port = o.MQTT.Port 281 bc.initServer(b) 282 return func() { 283 testMQTTShutdownServer(s) 284 testDisableRMSCache = prevDisableRMSCache 285 } 286 } 287 288 func (bc *mqttBenchContext) startCluster(b *testing.B, disableRMSCache bool) func() { 289 b.Helper() 290 b.StopTimer() 291 prevDisableRMSCache := testDisableRMSCache 292 testDisableRMSCache = disableRMSCache 293 conf := ` 294 listen: 127.0.0.1:-1 295 server_name: %s 296 jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'} 297 298 cluster { 299 name: %s 300 listen: 127.0.0.1:%d 301 routes = [%s] 302 } 303 304 mqtt { 305 listen: 127.0.0.1:-1 306 stream_replicas: 3 307 } 308 309 # For access to system account. 310 accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } } 311 ` 312 313 cl := createJetStreamClusterWithTemplate(b, conf, "MQTT", 3) 314 o := cl.randomNonLeader().getOpts() 315 bc.Host = o.MQTT.Host 316 bc.Port = o.MQTT.Port 317 bc.initServer(b) 318 return func() { 319 cl.shutdown() 320 testDisableRMSCache = prevDisableRMSCache 321 } 322 } 323 324 func mqttBenchWrapForMatrixField( 325 vFieldPtr *int, 326 arr []int, 327 f func(b *testing.B, bc *mqttBenchContext), 328 namef func(int) string, 329 ) func(b *testing.B, bc *mqttBenchContext) { 330 if len(arr) == 0 { 331 return f 332 } 333 return func(b *testing.B, bc *mqttBenchContext) { 334 for _, value := range arr { 335 *vFieldPtr = value 336 b.Run(namef(value), func(b *testing.B) { 337 f(b, bc) 338 }) 339 } 340 } 341 } 342 343 func (m mqttBenchMatrix) runMatrix(b *testing.B, bc mqttBenchContext, f func(*testing.B, *mqttBenchContext)) { 344 b.Helper() 345 f = mqttBenchWrapForMatrixField(&bc.MessageSize, m.MessageSize, f, func(size int) string { 346 return sizeKB(size) 347 }) 348 f = mqttBenchWrapForMatrixField(&bc.Topics, m.Topics, f, func(n int) string { 349 return fmt.Sprintf("%dtopics", n) 350 }) 351 f = mqttBenchWrapForMatrixField(&bc.Publishers, m.Publishers, f, func(n int) string { 352 return fmt.Sprintf("%dpubc", n) 353 }) 354 f = mqttBenchWrapForMatrixField(&bc.Subscribers, m.Subscribers, f, func(n int) string { 355 return fmt.Sprintf("%dsubc", n) 356 }) 357 f = mqttBenchWrapForMatrixField(&bc.QOS, m.QOS, f, func(qos int) string { 358 return fmt.Sprintf("QOS%d", qos) 359 }) 360 b.ResetTimer() 361 b.StartTimer() 362 f(b, &bc) 363 } 364 365 func (m mqttBenchMatrix) NoSubscribers() mqttBenchMatrix { 366 m.Subscribers = nil 367 return m 368 } 369 370 func (m mqttBenchMatrix) NoTopics() mqttBenchMatrix { 371 m.Topics = nil 372 return m 373 } 374 375 func (m mqttBenchMatrix) NoPublishers() mqttBenchMatrix { 376 m.Publishers = nil 377 return m 378 } 379 380 func (m mqttBenchMatrix) QOS0Only() mqttBenchMatrix { 381 m.QOS = []int{0} 382 return m 383 } 384 385 func (m mqttBenchMatrix) QOS1Only() mqttBenchMatrix { 386 m.QOS = []int{1} 387 return m 388 } 389 390 func sizeKB(size int) string { 391 unit := "B" 392 N := size 393 if size >= KB { 394 unit = "KB" 395 N = (N + KB/2) / KB 396 } 397 return fmt.Sprintf("%d%s", N, unit) 398 } 399 400 func (r MQTTBenchmarkResult) report(b *testing.B) { 401 // Disable the default ns metric in favor of custom X_ms/op. 402 b.ReportMetric(0, "ns/op") 403 404 // Disable MB/s since the github benchmarking action misinterprets the sign 405 // of the result (treats it as less is better). 406 b.SetBytes(0) 407 // b.SetBytes(r.Bytes) 408 409 for unit, ns := range r.NS { 410 nsOp := float64(ns) / float64(r.Ops) 411 b.ReportMetric(nsOp/1000000, unit+"_ms/op") 412 } 413 414 // Diable ReportAllocs() since it confuses the github benchmarking action 415 // with the noise. 416 // b.ReportAllocs() 417 } 418 419 func mqttNewBenchEx(b *testing.B) *mqttBenchContext { 420 cmd := mqttBenchLookupCommand(b, "mqtt-test") 421 return &mqttBenchContext{ 422 testCmdPath: cmd, 423 } 424 }