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  }