github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/integration/cluster_integration_tests.md (about) 1 # Cluster Integration Tests 2 3 Cluster integration tests are purely Go tests that allow us to test behavior within a single M3 component or across multiple components in a setup that closely resembles an actual M3 deployment. 4 5 ## Overview 6 7 ### Motivation & Use 8 Cluster integration tests were created to allow us to write tests that were faster to run, easier to write, and easier to debug than the previously docker-based counterparts. 9 10 These integration tests can be run via `go test` (and subsequently an IDE). They look similar to the standard unit test you'd write in Go (e.g. `func TestFoo(t *testing.T)`). 11 12 ### API 13 The integration test framework provides an API for creating and interacting with a cluster. The API and its implementation lives [here](https://github.com/m3db/m3/tree/master/src/integration/resources). This [types file](https://github.com/m3db/m3/blob/11a38384efb6d00f26536941e8265009931ead06/src/integration/resources/types.go#L55-L208) outlines the objects representing M3 components and the API calls that can be made to each component. Let's review the cluster interface: 14 15 _M3 Interface_ 16 17 ```golang 18 // M3Resources represents a set of test M3 components. 19 type M3Resources interface { 20 // Cleanup cleans up after each started component. 21 Cleanup() error 22 // Nodes returns all node resources. 23 Nodes() Nodes 24 // Coordinator returns the coordinator resource. 25 Coordinator() Coordinator 26 // Aggregators returns all aggregator resources. 27 Aggregators() Aggregators 28 } 29 ``` 30 An instantiation of the interface above gives us an M3 cluster to operate on. DB nodes, aggregators, and the coordinator can be manipulated as desired by retrieving the component and invoking API calls. See the types file above to see all the operations that can be done on each component. 31 32 33 ### When to Use 34 Consider writing a cluster integration test when any of the following apply: 35 36 * Test involves multiple parts of a component 37 * Test requires cross component communication 38 * Test can be driven entirely by component config and API calls 39 * Investigating an issue where you'd typically spin up a real cluster 40 * Local development 41 42 ### When Not to Use 43 There are some occasions where cluster integration tests may not be the best tool. Consider other options if: 44 45 * Test requires you to manipulate the clock. 46 * This is currently unsupported. Component-specific integration tests that are configured programmatically may be a better option. Here are some examples for [dbnodes](https://github.com/m3db/m3/tree/master/src/dbnode/integration) 47 * Test requires changing options not exposed by config. 48 * Since cluster integration tests start M3 components via the same entry point as the actual binary, all configuration has to be done via the component configuration file or public APIs. If you need to manipulate some options that are not exposed this way, then these tests may not be a good fit. 49 50 ## Example Usage 51 52 Let's walk through a few examples that outline the most common use cases. 53 54 ### Denoting test as a cluster integration test 55 Any test meant to be considered as a cluster integration test should be tagged with the correct build tag: 56 57 ```golang 58 // +build cluster_integration 59 ``` 60 61 ### Spinning up a cluster 62 63 64 Below is an example that will spin up a cluster with a coordinator, DB node, and aggregator that can interact with each other. It also creates you an unaggregated and an aggregated namespace that you can read and write to. 65 66 ```golang 67 import ( 68 "github.com/m3db/m3/src/integration/resources" 69 "github.com/m3db/m3/src/integration/resources/inprocess" 70 ) 71 72 // The {} represent empty configs which will start each component 73 // with the default configuration 74 cfgs, _ := inprocess.NewClusterConfigsFromYAML( 75 `db: {}`, `{}`, `{}`, 76 ) 77 78 m3, _ = inprocess.NewCluster(cfgs, 79 resources.ClusterOptions{ 80 DBNode: resources.NewDBNodeClusterOptions(), 81 Aggregator: resources.NewAggregatorClusterOptions() 82 }, 83 ) 84 ``` 85 86 Naturally, you can spin the cluster up with whatever configuration you like. Also, you can use `DBNodeClusterOptions` and `AggregatorClusterOptions` to spin up more interesting clusters. For example: 87 88 ```golang 89 m3, _ := inprocess.NewCluster(configs, resources.ClusterOptions{ 90 DBNode: &resources.DBNodeClusterOptions{ 91 RF: 3, 92 NumInstances: 1, 93 NumShards: 4, 94 NumIsolationGroups: 3, 95 }, 96 Aggregator: &resources.AggregatorClusterOptions{ 97 RF: 2, 98 NumShards: 4, 99 NumInstances: 2, 100 NumIsolationGroups: 2, 101 } 102 }) 103 ``` 104 This configuration will spin up the following: 105 106 * DB nodes with an RF of 3 and 1 instance for each RF (i.e. 3 separate invocations of M3DB in a single process) 107 * 1 coordinator 108 * Aggregators with an RF of 2 and 2 instances for each RF (i.e. 4 separate aggregator invocations in a single process) 109 110 It's worth pointing out that you aren't required to spin up a full cluster each time. `inprocess.NewCluster` also allows you to just spin up a dbnode and coordinator, and later we demonstrate how to spin up just a single component. 111 112 ### Reading and writing to a cluster 113 Continuing from where we left off in the [Spinning up a cluster](#Spinning up a cluster) section, let's read and write some data to the new cluster. 114 115 ```golang 116 // Write some data 117 coordinator := m3.Coordinator() 118 119 /* 120 * Using this method on the resources.Coordinator interface: 121 * 122 * // WriteProm writes a prometheus metric. Takes tags/labels as a map for convenience. 123 * WriteProm(name string, tags map[string]string, samples []prompb.Sample, headers Headers) error 124 * 125 */ 126 _ := coordinator.WriteProm("foo_metric", map[string]string{ 127 "bar_label": "baz", 128 }, []prompb.Sample{ 129 { 130 Value: 42, 131 Timestamp: storage.TimeToPromTimestamp(xtime.Now()), 132 }, 133 }, nil) 134 135 // Read some data 136 /* 137 * Using this method on the resources.Coordinator interface: 138 * 139 * // RangeQuery runs a range query with provided headers 140 * RangeQuery(req RangeQueryRequest, headers Headers) (model.Matrix, error) 141 * 142 */ 143 result, err := coord.RangeQuery( 144 resources.RangeQueryRequest{ 145 Query: "foo_metric", 146 Start: time.Now().Add(-30 * time.Second), 147 End: time.Now(), 148 }, 149 nil) 150 151 ``` 152 NB: If using this code in tests, the `RangeQuery` may need to be retried to yield a result. Writes in M3 are async by default, so they're not immediately available for reads. `resources.Retry` is provided for convenience to assist with this. 153 154 ### Spinning up an external resource 155 Occasionally, it is convenient to test M3 alongside some component it works closely with. Prometheus is the most common example. The cluster integration test framework supports this by spinning up external resources in docker containers. External resources must adhere to the following interface: 156 157 ```golang 158 // ExternalResources represents an external (i.e. non-M3) 159 // resource that we'd like to be able to spin up for an 160 // integration test. 161 type ExternalResources interface { 162 // Setup sets up the external resource so that it's ready 163 // for use. 164 Setup() error 165 166 // Close stops and cleans up all the resources associated with 167 // the external resource. 168 Close() error 169 } 170 ``` 171 172 Since it's so commonly used in conjunction with M3, an implementation for [Prometheus](https://github.com/m3db/m3/blob/master/src/integration/resources/docker/prometheus.go) already exists. 173 174 Here's an example of a test spinning up M3 and Prometheus: 175 176 ```golang 177 cfgs, _ := inprocess.NewClusterConfigsFromConfigFile(pathToDBCfg, pathToCoordCfg, "") 178 m3, _ := inprocess.NewCluster(cfgs, 179 resources.ClusterOptions{ 180 DBNode: resources.NewDBNodeClusterOptions(), 181 }, 182 ) 183 defer m3.Cleanup() 184 185 // Spin up external resources. In this case, prometheus. 186 pool, _ := dockertest.NewPool("") 187 188 prom := docker.NewPrometheus(docker.PrometheusOptions{ 189 Pool: pool, 190 PathToCfg: pathToPromCfg, 191 }) 192 prom.Setup() 193 defer prom.Close() 194 195 // Run tests... 196 197 198 ``` 199 200 ### Spinning up an individual component 201 We've been mostly referring to a cluster with a coordinator, db node, and potentially an aggregator, but it's also possible to spin up each component individually. Each component has the same handful of constructors in the `inprocess` [package](https://github.com/m3db/m3/tree/master/src/integration/resources/inprocess). Here's an example of starting up a DB node. 202 203 ```golang 204 dbnode, _ := inprocess.NewDBNodeFromYAML(dbnodeCfg, inprocess.DBNodeOptions{}) 205 206 ``` 207 208 ## More Information 209 * [Some](https://github.com/m3db/m3/tree/master/src/integration/simple) [example](https://github.com/m3db/m3/tree/master/src/integration/repair) [tests](https://github.com/m3db/m3/tree/master/src/integration/prometheus) 210 * [Component APIs](https://github.com/m3db/m3/blob/1f98da6c2addca3001ff7b7b7a00a99a2c70bbbb/src/integration/resources/types.go#L55-L183)