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  }