github.com/m3db/m3@v1.5.0/src/cluster/placementhandler/delete_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 "fmt" 26 "io/ioutil" 27 "net/http" 28 "net/http/httptest" 29 "testing" 30 "time" 31 32 "github.com/m3db/m3/src/aggregator/aggregator" 33 "github.com/m3db/m3/src/cluster/kv" 34 "github.com/m3db/m3/src/cluster/placement" 35 "github.com/m3db/m3/src/cluster/placementhandler/handleroptions" 36 "github.com/m3db/m3/src/cluster/shard" 37 "github.com/m3db/m3/src/x/instrument" 38 xtest "github.com/m3db/m3/src/x/test" 39 40 "github.com/golang/mock/gomock" 41 "github.com/gorilla/mux" 42 "github.com/stretchr/testify/assert" 43 "github.com/stretchr/testify/require" 44 ) 45 46 func TestPlacementDeleteHandler_Force(t *testing.T) { 47 runForAllAllowedServices(func(serviceName string) { 48 ctrl := xtest.NewController(t) 49 defer ctrl.Finish() 50 51 mockClient, mockPlacementService := SetupPlacementTest(t, ctrl) 52 handlerOpts, err := NewHandlerOptions(mockClient, 53 placement.Configuration{}, nil, instrument.NewOptions()) 54 handler := NewDeleteHandler(handlerOpts) 55 56 svcDefaults := handleroptions.ServiceNameAndDefaults{ 57 ServiceName: serviceName, 58 } 59 60 // Test remove success 61 inst := placement.NewInstance(). 62 SetID("host1"). 63 SetEndpoint("host1:123"). 64 SetHostname("host1"). 65 SetPort(123). 66 SetIsolationGroup("host1-group"). 67 SetWeight(1). 68 SetZone("default") 69 if serviceName == handleroptions.M3AggregatorServiceName { 70 inst = inst.SetShardSetID(0) 71 } 72 73 existing := placement.NewPlacement(). 74 SetInstances([]placement.Instance{inst}) 75 76 mockKVStore := kv.NewMockStore(ctrl) 77 78 w := httptest.NewRecorder() 79 req := httptest. 80 NewRequest(DeleteHTTPMethod, "/placement/host1?force=true", nil) 81 req = mux.SetURLVars(req, map[string]string{"id": "host1"}) 82 require.NotNil(t, req) 83 mockPlacementService.EXPECT().Placement().Return(existing, nil) 84 mockPlacementService.EXPECT().RemoveInstances([]string{"host1"}). 85 Return(placement.NewPlacement(), nil) 86 if serviceName == handleroptions.M3AggregatorServiceName { 87 flushTimesMgrOpts := aggregator.NewFlushTimesManagerOptions() 88 electionMgrOpts := aggregator.NewElectionManagerOptions() 89 mockClient.EXPECT().Store(gomock.Any()).Return(mockKVStore, nil) 90 mockKVStore.EXPECT(). 91 Get(gomock.Any()). 92 DoAndReturn(func(k string) (kv.Value, error) { 93 switch k { 94 case fmt.Sprintf(flushTimesMgrOpts.FlushTimesKeyFmt(), inst.ShardSetID()): 95 return nil, nil 96 case fmt.Sprintf(electionMgrOpts.ElectionKeyFmt(), inst.ShardSetID()): 97 return nil, nil 98 } 99 return nil, errors.New("unexpected") 100 }). 101 AnyTimes() 102 mockKVStore.EXPECT(). 103 Delete(gomock.Any()). 104 DoAndReturn(func(k string) (kv.Value, error) { 105 switch k { 106 case fmt.Sprintf(flushTimesMgrOpts.FlushTimesKeyFmt(), inst.ShardSetID()): 107 return nil, nil 108 case fmt.Sprintf(electionMgrOpts.ElectionKeyFmt(), inst.ShardSetID()): 109 return nil, nil 110 } 111 return nil, errors.New("unexpected") 112 }). 113 AnyTimes() 114 } 115 116 handler.ServeHTTP(svcDefaults, w, req) 117 118 resp := w.Result() 119 body, err := ioutil.ReadAll(resp.Body) 120 require.NoError(t, err) 121 require.Equal(t, http.StatusOK, resp.StatusCode) 122 require.Equal(t, `{"placement":{"instances":{},"replicaFactor":0,"numShards":0,"isSharded":false,"cutoverTime":"0","isMirrored":false,"maxShardSetId":0},"version":0}`, string(body)) 123 124 // Test remove failure 125 w = httptest.NewRecorder() 126 req = httptest.NewRequest(DeleteHTTPMethod, "/placement/nope?force=true", nil) 127 req = mux.SetURLVars(req, map[string]string{"id": "nope"}) 128 require.NotNil(t, req) 129 mockPlacementService.EXPECT().Placement().Return(existing, nil) 130 handler.ServeHTTP(svcDefaults, w, req) 131 132 resp = w.Result() 133 body, err = ioutil.ReadAll(resp.Body) 134 require.NoError(t, err) 135 require.Equal(t, http.StatusNotFound, resp.StatusCode) 136 require.JSONEq(t, `{"status":"error","error":"instance not found: nope"}`, string(body)) 137 }) 138 } 139 140 func TestPlacementDeleteHandler_Safe(t *testing.T) { 141 runForAllAllowedServices(func(serviceName string) { 142 t.Run(serviceName, func(t *testing.T) { 143 testDeleteHandlerSafe(t, serviceName) 144 }) 145 }) 146 } 147 148 func testDeleteHandlerSafe(t *testing.T, serviceName string) { 149 ctrl := xtest.NewController(t) 150 defer ctrl.Finish() 151 152 mockClient, mockPlacementService := SetupPlacementTest(t, ctrl) 153 handlerOpts, err := NewHandlerOptions( 154 mockClient, 155 placement.Configuration{}, 156 &handleroptions.M3AggServiceOptions{ 157 WarmupDuration: time.Minute, 158 MaxAggregationWindowSize: 5 * time.Minute, 159 }, 160 instrument.NewOptions()) 161 require.NoError(t, err) 162 163 mockKVStore := kv.NewMockStore(ctrl) 164 shardSetIDs := []uint32{0, 1} 165 166 if serviceName == handleroptions.M3AggregatorServiceName { 167 flushTimesMgrOpts := aggregator.NewFlushTimesManagerOptions() 168 electionMgrOpts := aggregator.NewElectionManagerOptions() 169 mockClient.EXPECT().Store(gomock.Any()).Return(mockKVStore, nil).AnyTimes() 170 mockKVStore.EXPECT(). 171 Get(gomock.Any()). 172 DoAndReturn(func(k string) (kv.Value, error) { 173 for _, shardSetID := range shardSetIDs { 174 switch k { 175 case fmt.Sprintf(flushTimesMgrOpts.FlushTimesKeyFmt(), shardSetID): 176 return nil, nil 177 case fmt.Sprintf(electionMgrOpts.ElectionKeyFmt(), shardSetID): 178 return nil, nil 179 } 180 } 181 return nil, errors.New("unexpected") 182 }). 183 AnyTimes() 184 mockKVStore.EXPECT(). 185 Delete(gomock.Any()). 186 DoAndReturn(func(k string) (kv.Value, error) { 187 for _, shardSetID := range shardSetIDs { 188 switch k { 189 case fmt.Sprintf(flushTimesMgrOpts.FlushTimesKeyFmt(), shardSetID): 190 return nil, nil 191 case fmt.Sprintf(electionMgrOpts.ElectionKeyFmt(), shardSetID): 192 return nil, nil 193 } 194 } 195 return nil, errors.New("unexpected") 196 }). 197 AnyTimes() 198 } 199 200 var ( 201 handler = NewDeleteHandler(handlerOpts) 202 203 basePlacement = placement.NewPlacement(). 204 SetIsSharded(true) 205 206 // Test remove absent host 207 w = httptest.NewRecorder() 208 req = httptest.NewRequest(DeleteHTTPMethod, "/placement/host1", nil) 209 ) 210 handler.nowFn = func() time.Time { return time.Unix(0, 0) } 211 212 switch serviceName { 213 case handleroptions.M3CoordinatorServiceName: 214 basePlacement = basePlacement. 215 SetIsSharded(false). 216 SetReplicaFactor(1) 217 case handleroptions.M3AggregatorServiceName: 218 basePlacement = basePlacement. 219 SetIsMirrored(true) 220 } 221 222 svcDefaults := handleroptions.ServiceNameAndDefaults{ 223 ServiceName: serviceName, 224 } 225 226 req = mux.SetURLVars(req, map[string]string{"id": "host1"}) 227 require.NotNil(t, req) 228 mockPlacementService.EXPECT().Placement().Return(basePlacement, nil) 229 handler.ServeHTTP(svcDefaults, w, req) 230 231 resp := w.Result() 232 body, err := ioutil.ReadAll(resp.Body) 233 require.NoError(t, err) 234 assert.Contains(t, string(body), "instance not found: host1") 235 assert.Equal(t, http.StatusNotFound, resp.StatusCode) 236 237 // Test remove host when placement unsafe 238 basePlacement = basePlacement.SetInstances([]placement.Instance{ 239 placement.NewInstance(). 240 SetID("host1"). 241 SetShards(shard.NewShards([]shard.Shard{ 242 shard.NewShard(2).SetState(shard.Available), 243 })). 244 SetShardSetID(shardSetIDs[0]), 245 placement.NewInstance(). 246 SetID("host2"). 247 SetShards(shard.NewShards([]shard.Shard{ 248 shard.NewShard(1).SetState(shard.Leaving), 249 })). 250 SetShardSetID(shardSetIDs[1]), 251 }) 252 253 switch serviceName { 254 case handleroptions.M3CoordinatorServiceName: 255 // M3Coordinator placement changes are alway safe because it is stateless 256 default: 257 w = httptest.NewRecorder() 258 req = httptest.NewRequest(DeleteHTTPMethod, "/placement/host1", nil) 259 req = mux.SetURLVars(req, map[string]string{"id": "host1"}) 260 require.NotNil(t, req) 261 mockPlacementService.EXPECT().Placement().Return(basePlacement, nil) 262 handler.ServeHTTP(svcDefaults, w, req) 263 264 resp = w.Result() 265 body, err = ioutil.ReadAll(resp.Body) 266 require.NoError(t, err) 267 require.Equal(t, http.StatusBadRequest, resp.StatusCode) 268 require.JSONEq(t, 269 `{"status":"error","error":"instances do not have all shards available: [host2]"}`, 270 string(body)) 271 } 272 273 // Test OK 274 basePlacement = basePlacement.SetReplicaFactor(2).SetMaxShardSetID(2).SetInstances([]placement.Instance{ 275 placement.NewInstance().SetID("host1").SetIsolationGroup("a").SetWeight(10). 276 SetShards(shard.NewShards([]shard.Shard{ 277 shard.NewShard(0).SetState(shard.Available), 278 })), 279 placement.NewInstance().SetID("host2").SetIsolationGroup("b").SetWeight(10). 280 SetShards(shard.NewShards([]shard.Shard{ 281 shard.NewShard(0).SetState(shard.Available), 282 shard.NewShard(1).SetState(shard.Available), 283 })), 284 placement.NewInstance().SetID("host3").SetIsolationGroup("c").SetWeight(10). 285 SetShards(shard.NewShards([]shard.Shard{ 286 shard.NewShard(1).SetState(shard.Available), 287 })), 288 }) 289 290 var returnPlacement placement.Placement 291 292 switch serviceName { 293 case handleroptions.M3CoordinatorServiceName: 294 basePlacement. 295 SetIsSharded(false). 296 SetReplicaFactor(1). 297 SetShards(nil). 298 SetInstances([]placement.Instance{placement.NewInstance().SetID("host1")}) 299 mockPlacementService.EXPECT().Placement().Return(basePlacement, nil) 300 mockPlacementService.EXPECT(). 301 RemoveInstances([]string{"host1"}). 302 Return(placement.NewPlacement(), nil) 303 case handleroptions.M3AggregatorServiceName: 304 // Need to be mirrored in M3Agg case 305 basePlacement.SetReplicaFactor(1).SetMaxShardSetID(2).SetInstances([]placement.Instance{ 306 placement.NewInstance().SetID("host1").SetIsolationGroup("a").SetWeight(10).SetShardSetID(0). 307 SetShards(shard.NewShards([]shard.Shard{ 308 shard.NewShard(0).SetState(shard.Available), 309 })), 310 placement.NewInstance().SetID("host2").SetIsolationGroup("b").SetWeight(10).SetShardSetID(1). 311 SetShards(shard.NewShards([]shard.Shard{ 312 shard.NewShard(1).SetState(shard.Available), 313 })), 314 }) 315 316 returnPlacement = basePlacement.Clone().SetInstances([]placement.Instance{ 317 placement.NewInstance().SetID("host1").SetIsolationGroup("a").SetWeight(10).SetShardSetID(0). 318 SetShards(shard.NewShards([]shard.Shard{ 319 shard.NewShard(0).SetState(shard.Leaving). 320 SetCutoffNanos(300000000000), 321 })), 322 placement.NewInstance().SetID("host2").SetIsolationGroup("b").SetWeight(10).SetShardSetID(1). 323 SetShards(shard.NewShards([]shard.Shard{ 324 shard.NewShard(0).SetState(shard.Initializing). 325 SetCutoverNanos(300000000000). 326 SetSourceID("host1"), 327 shard.NewShard(1).SetState(shard.Available), 328 })), 329 }).SetVersion(2) 330 case handleroptions.M3DBServiceName: 331 returnPlacement = basePlacement.Clone().SetInstances([]placement.Instance{ 332 placement.NewInstance().SetID("host1").SetIsolationGroup("a").SetWeight(10). 333 SetShards(shard.NewShards([]shard.Shard{ 334 shard.NewShard(0).SetState(shard.Leaving), 335 })), 336 placement.NewInstance().SetID("host2").SetIsolationGroup("b").SetWeight(10). 337 SetShards(shard.NewShards([]shard.Shard{ 338 shard.NewShard(0).SetState(shard.Available), 339 shard.NewShard(1).SetState(shard.Available), 340 })), 341 placement.NewInstance().SetID("host3").SetIsolationGroup("c").SetWeight(10). 342 SetShards(shard.NewShards([]shard.Shard{ 343 shard.NewShard(0).SetState(shard.Initializing).SetSourceID("host1"), 344 shard.NewShard(1).SetState(shard.Available), 345 })), 346 }).SetVersion(2) 347 } 348 349 w = httptest.NewRecorder() 350 req = httptest.NewRequest(DeleteHTTPMethod, "/placement/host1", nil) 351 req = mux.SetURLVars(req, map[string]string{"id": "host1"}) 352 require.NotNil(t, req) 353 354 if !isStateless(serviceName) { 355 mockPlacementService.EXPECT().Placement().Return(basePlacement, nil) 356 mockPlacementService.EXPECT().CheckAndSet(gomock.Any(), 0).Return(returnPlacement, nil) 357 } 358 359 handler.ServeHTTP(svcDefaults, w, req) 360 361 resp = w.Result() 362 body, err = ioutil.ReadAll(resp.Body) 363 require.NoError(t, err) 364 switch serviceName { 365 case handleroptions.M3CoordinatorServiceName: 366 require.Equal(t, `{"placement":{"instances":{},"replicaFactor":0,"numShards":0,"isSharded":false,"cutoverTime":"0","isMirrored":false,"maxShardSetId":0},"version":0}`, string(body)) // nolint:lll 367 case handleroptions.M3AggregatorServiceName: 368 require.Equal(t, `{"placement":{"instances":{"host1":{"id":"host1","isolationGroup":"a","zone":"","weight":10,"endpoint":"","shards":[{"id":0,"state":"LEAVING","sourceId":"","cutoverNanos":"0","cutoffNanos":"300000000000","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"host2":{"id":"host2","isolationGroup":"b","zone":"","weight":10,"endpoint":"","shards":[{"id":0,"state":"INITIALIZING","sourceId":"host1","cutoverNanos":"300000000000","cutoffNanos":"0","redirectToShardId":null},{"id":1,"state":"AVAILABLE","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":1,"hostname":"","port":0,"metadata":{"debugPort":0}}},"replicaFactor":1,"numShards":0,"isSharded":true,"cutoverTime":"0","isMirrored":true,"maxShardSetId":2},"version":2}`, string(body)) // nolint:lll 369 default: 370 require.Equal(t, `{"placement":{"instances":{"host1":{"id":"host1","isolationGroup":"a","zone":"","weight":10,"endpoint":"","shards":[{"id":0,"state":"LEAVING","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"host2":{"id":"host2","isolationGroup":"b","zone":"","weight":10,"endpoint":"","shards":[{"id":0,"state":"AVAILABLE","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null},{"id":1,"state":"AVAILABLE","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}},"host3":{"id":"host3","isolationGroup":"c","zone":"","weight":10,"endpoint":"","shards":[{"id":0,"state":"INITIALIZING","sourceId":"host1","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null},{"id":1,"state":"AVAILABLE","sourceId":"","cutoverNanos":"0","cutoffNanos":"0","redirectToShardId":null}],"shardSetId":0,"hostname":"","port":0,"metadata":{"debugPort":0}}},"replicaFactor":2,"numShards":0,"isSharded":true,"cutoverTime":"0","isMirrored":false,"maxShardSetId":2},"version":2}`, string(body)) // nolint:lll 371 } 372 }