github.com/m3db/m3@v1.5.0/src/cluster/placementhandler/replace_test.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package placementhandler 22 23 import ( 24 "errors" 25 "io/ioutil" 26 "net/http" 27 "net/http/httptest" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/m3db/m3/src/cluster/placement" 33 "github.com/m3db/m3/src/cluster/placementhandler/handleroptions" 34 "github.com/m3db/m3/src/cluster/shard" 35 "github.com/m3db/m3/src/x/instrument" 36 37 "github.com/golang/mock/gomock" 38 "github.com/stretchr/testify/assert" 39 "github.com/stretchr/testify/require" 40 ) 41 42 func newReplaceRequest(body string) *http.Request { 43 rb := strings.NewReader(body) 44 return httptest.NewRequest(ReplaceHTTPMethod, M3DBReplaceURL, rb) 45 } 46 47 func TestPlacementReplaceHandler_Force(t *testing.T) { 48 runForAllAllowedServices(func(s string) { 49 t.Run(s, func(t *testing.T) { 50 testPlacementReplaceHandlerForce(t, s) 51 }) 52 }) 53 } 54 55 func TestPlacementReplaceHandler_Safe_Err(t *testing.T) { 56 runForAllAllowedServices(func(s string) { 57 t.Run(s, func(t *testing.T) { 58 testPlacementReplaceHandlerSafeErr(t, s) 59 }) 60 }) 61 } 62 63 func TestPlacementReplaceHandler_Safe_Ok(t *testing.T) { 64 runForAllAllowedServices(func(s string) { 65 t.Run(s, func(t *testing.T) { 66 testPlacementReplaceHandlerSafeOk(t, s) 67 }) 68 }) 69 } 70 71 func testPlacementReplaceHandlerForce(t *testing.T, serviceName string) { 72 ctrl := gomock.NewController(t) 73 defer ctrl.Finish() 74 75 mockClient, mockPlacementService := SetupPlacementTest(t, ctrl) 76 handlerOpts, err := NewHandlerOptions(mockClient, placement.Configuration{}, nil, instrument.NewOptions()) 77 require.NoError(t, err) 78 handler := NewReplaceHandler(handlerOpts) 79 handler.nowFn = func() time.Time { return time.Unix(0, 0) } 80 81 w := httptest.NewRecorder() 82 req := newReplaceRequest(`{"force": true, "leavingInstanceIDs": []}`) 83 84 svcDefaults := handleroptions.ServiceNameAndDefaults{ 85 ServiceName: serviceName, 86 } 87 88 mockPlacementService.EXPECT().ReplaceInstances([]string{}, gomock.Any()).Return(placement.NewPlacement(), nil, errors.New("test")) 89 handler.ServeHTTP(svcDefaults, w, req) 90 91 resp := w.Result() 92 body, _ := ioutil.ReadAll(resp.Body) 93 assert.JSONEq(t, `{"status":"error","error":"test"}`, string(body)) 94 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 95 96 w = httptest.NewRecorder() 97 req = newReplaceRequest(`{"force": true, "leavingInstanceIDs": ["a"]}`) 98 mockPlacementService.EXPECT().ReplaceInstances([]string{"a"}, gomock.Not(nil)).Return(placement.NewPlacement(), nil, nil) 99 handler.ServeHTTP(svcDefaults, w, req) 100 resp = w.Result() 101 body, _ = ioutil.ReadAll(resp.Body) 102 assert.Equal(t, `{"placement":{"instances":{},"replicaFactor":0,"numShards":0,"isSharded":false,"cutoverTime":"0","isMirrored":false,"maxShardSetId":0},"version":0}`, string(body)) 103 assert.Equal(t, http.StatusOK, resp.StatusCode) 104 } 105 106 func testPlacementReplaceHandlerSafeErr(t *testing.T, serviceName string) { 107 ctrl := gomock.NewController(t) 108 defer ctrl.Finish() 109 110 mockClient, mockPlacementService := SetupPlacementTest(t, ctrl) 111 handlerOpts, err := NewHandlerOptions(mockClient, placement.Configuration{}, nil, instrument.NewOptions()) 112 require.NoError(t, err) 113 handler := NewReplaceHandler(handlerOpts) 114 handler.nowFn = func() time.Time { return time.Unix(0, 0) } 115 116 w := httptest.NewRecorder() 117 req := newReplaceRequest("{}") 118 119 svcDefaults := handleroptions.ServiceNameAndDefaults{ 120 ServiceName: serviceName, 121 } 122 123 mockPlacementService.EXPECT().Placement().Return(newInitPlacement(), nil) 124 if serviceName == handleroptions.M3CoordinatorServiceName { 125 mockPlacementService.EXPECT().CheckAndSet(gomock.Any(), 0). 126 Return(newInitPlacement().SetVersion(1), nil) 127 } 128 129 handler.ServeHTTP(svcDefaults, w, req) 130 resp := w.Result() 131 body, _ := ioutil.ReadAll(resp.Body) 132 133 switch serviceName { 134 case handleroptions.M3CoordinatorServiceName: 135 assert.Equal(t, http.StatusOK, resp.StatusCode) 136 default: 137 assert.Equal(t, http.StatusBadRequest, resp.StatusCode) 138 assert.JSONEq(t, 139 `{"status":"error","error":"instances do not have all shards available: [A, B]"}`, 140 string(body)) 141 } 142 } 143 144 type placementReplaceMatcher struct{} 145 146 func (placementReplaceMatcher) Matches(x interface{}) bool { 147 pl := x.(placement.Placement) 148 149 instA, ok := pl.Instance("A") 150 if !ok { 151 return false 152 } 153 154 instC, ok := pl.Instance("C") 155 if !ok { 156 return false 157 } 158 159 return instA.Shards().NumShardsForState(shard.Leaving) == 1 && 160 instC.Shards().NumShardsForState(shard.Initializing) == 1 161 } 162 163 func (placementReplaceMatcher) String() string { 164 return "matches if the placement has instance A leaving and C initializing" 165 } 166 167 func newPlacementReplaceMatcher() gomock.Matcher { 168 return placementReplaceMatcher{} 169 } 170 171 func testPlacementReplaceHandlerSafeOk(t *testing.T, serviceName string) { 172 ctrl := gomock.NewController(t) 173 defer ctrl.Finish() 174 175 mockClient, mockPlacementService := SetupPlacementTest(t, ctrl) 176 handlerOpts, err := NewHandlerOptions(mockClient, placement.Configuration{}, nil, instrument.NewOptions()) 177 require.NoError(t, err) 178 handler := NewReplaceHandler(handlerOpts) 179 handler.nowFn = func() time.Time { return time.Unix(0, 0) } 180 181 pl := newAvailPlacement() 182 183 matcher := gomock.Any() 184 switch serviceName { 185 case handleroptions.M3DBServiceName: 186 pl = pl.SetIsSharded(true) 187 matcher = newPlacementReplaceMatcher() 188 case handleroptions.M3AggregatorServiceName: 189 pl = pl.SetIsSharded(true).SetIsMirrored(true) 190 matcher = newPlacementReplaceMatcher() 191 default: 192 } 193 194 instances := pl.Instances() 195 for i, inst := range instances { 196 newInst := inst.SetIsolationGroup("r1").SetZone("z1").SetWeight(1) 197 if serviceName == handleroptions.M3CoordinatorServiceName { 198 newInst = newInst.SetShards(shard.NewShards([]shard.Shard{})) 199 } 200 instances[i] = newInst 201 } 202 203 pl = pl.SetInstances(instances).SetVersion(1) 204 w := httptest.NewRecorder() 205 req := newReplaceRequest(` 206 { 207 "leavingInstanceIDs": ["A"], 208 "candidates": [ 209 { 210 "id": "C", 211 "zone": "z1", 212 "isolation_group": "r1", 213 "weight": 1 214 } 215 ] 216 } 217 `) 218 219 mockPlacementService.EXPECT().Placement().Return(pl.Clone(), nil) 220 221 returnPl := pl.Clone() 222 223 newInst := placement.NewInstance(). 224 SetID("C"). 225 SetZone("z1"). 226 SetIsolationGroup("r1"). 227 SetWeight(1) 228 229 if !isStateless(serviceName) { 230 newInst = newInst.SetShards(shard.NewShards([]shard.Shard{ 231 shard.NewShard(1). 232 SetState(shard.Initializing). 233 SetSourceID("A"), 234 })) 235 } 236 237 instances = append(returnPl.Instances(), newInst) 238 239 instances[0].Shards().Remove(1) 240 instances[0].Shards().Add(shard.NewShard(1).SetState(shard.Leaving)) 241 242 if isStateless(serviceName) { 243 instances = instances[1:] 244 } 245 246 returnPl = returnPl. 247 SetInstances(instances). 248 SetVersion(2) 249 250 svcDefaults := handleroptions.ServiceNameAndDefaults{ 251 ServiceName: serviceName, 252 } 253 254 mockPlacementService.EXPECT().CheckAndSet(matcher, 1).Return(returnPl, nil) 255 handler.ServeHTTP(svcDefaults, w, req) 256 257 resp := w.Result() 258 body, _ := ioutil.ReadAll(resp.Body) 259 260 switch serviceName { 261 case handleroptions.M3CoordinatorServiceName: 262 exp := `{"placement":{"instances":{"B":{"id":"B","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"C":{"id":"C","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}}},"replicaFactor":0,"numShards":0,"isSharded":false,"cutoverTime":"0","isMirrored":false,"maxShardSetId":0},"version":2}` // nolint:lll 263 assert.Equal(t, exp, string(body)) 264 case handleroptions.M3DBServiceName: 265 exp := `{"placement":{"instances":{"A":{"id":"A","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[{"id":1,"state":"LEAVING","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"B":{"id":"B","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[{"id":1,"state":"AVAILABLE","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"C":{"id":"C","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[{"id":1,"state":"INITIALIZING","sourceId":"A","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}}},"replicaFactor":0,"numShards":0,"isSharded":true,"cutoverTime":"0","isMirrored":false,"maxShardSetId":0},"version":2}` // nolint:lll 266 assert.Equal(t, exp, string(body)) 267 case handleroptions.M3AggregatorServiceName: 268 exp := `{"placement":{"instances":{"A":{"id":"A","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[{"id":1,"state":"LEAVING","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"B":{"id":"B","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[{"id":1,"state":"AVAILABLE","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"C":{"id":"C","isolationGroup":"r1","zone":"z1","weight":1,"endpoint":"","shards":[{"id":1,"state":"INITIALIZING","sourceId":"A","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}}},"replicaFactor":0,"numShards":0,"isSharded":true,"cutoverTime":"0","isMirrored":true,"maxShardSetId":0},"version":2}` // nolint:lll 269 assert.Equal(t, exp, string(body)) 270 default: 271 t.Errorf("unknown service name %s", serviceName) 272 } 273 274 assert.Equal(t, http.StatusOK, resp.StatusCode) 275 }