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  }