github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/frontend/v2/frontend_test.go (about)

     1  package v2
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"strconv"
     7  	"strings"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/grafana/dskit/flagext"
    14  	"github.com/grafana/dskit/services"
    15  	"github.com/stretchr/testify/require"
    16  	"github.com/weaveworks/common/httpgrpc"
    17  	"github.com/weaveworks/common/user"
    18  	"go.uber.org/atomic"
    19  	"google.golang.org/grpc"
    20  
    21  	"github.com/cortexproject/cortex/pkg/frontend/v2/frontendv2pb"
    22  	"github.com/cortexproject/cortex/pkg/querier/stats"
    23  	"github.com/cortexproject/cortex/pkg/scheduler/schedulerpb"
    24  	"github.com/cortexproject/cortex/pkg/util/test"
    25  )
    26  
    27  const testFrontendWorkerConcurrency = 5
    28  
    29  func setupFrontend(t *testing.T, schedulerReplyFunc func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend) (*Frontend, *mockScheduler) {
    30  	l, err := net.Listen("tcp", "")
    31  	require.NoError(t, err)
    32  
    33  	server := grpc.NewServer()
    34  
    35  	h, p, err := net.SplitHostPort(l.Addr().String())
    36  	require.NoError(t, err)
    37  
    38  	grpcPort, err := strconv.Atoi(p)
    39  	require.NoError(t, err)
    40  
    41  	cfg := Config{}
    42  	flagext.DefaultValues(&cfg)
    43  	cfg.SchedulerAddress = l.Addr().String()
    44  	cfg.WorkerConcurrency = testFrontendWorkerConcurrency
    45  	cfg.Addr = h
    46  	cfg.Port = grpcPort
    47  
    48  	//logger := log.NewLogfmtLogger(os.Stdout)
    49  	logger := log.NewNopLogger()
    50  	f, err := NewFrontend(cfg, logger, nil)
    51  	require.NoError(t, err)
    52  
    53  	frontendv2pb.RegisterFrontendForQuerierServer(server, f)
    54  
    55  	ms := newMockScheduler(t, f, schedulerReplyFunc)
    56  	schedulerpb.RegisterSchedulerForFrontendServer(server, ms)
    57  
    58  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), f))
    59  	t.Cleanup(func() {
    60  		_ = services.StopAndAwaitTerminated(context.Background(), f)
    61  	})
    62  
    63  	go func() {
    64  		_ = server.Serve(l)
    65  	}()
    66  
    67  	t.Cleanup(func() {
    68  		_ = l.Close()
    69  	})
    70  
    71  	// Wait for frontend to connect to scheduler.
    72  	test.Poll(t, 1*time.Second, 1, func() interface{} {
    73  		ms.mu.Lock()
    74  		defer ms.mu.Unlock()
    75  
    76  		return len(ms.frontendAddr)
    77  	})
    78  
    79  	return f, ms
    80  }
    81  
    82  func sendResponseWithDelay(f *Frontend, delay time.Duration, userID string, queryID uint64, resp *httpgrpc.HTTPResponse) {
    83  	if delay > 0 {
    84  		time.Sleep(delay)
    85  	}
    86  
    87  	ctx := user.InjectOrgID(context.Background(), userID)
    88  	_, _ = f.QueryResult(ctx, &frontendv2pb.QueryResultRequest{
    89  		QueryID:      queryID,
    90  		HttpResponse: resp,
    91  		Stats:        &stats.Stats{},
    92  	})
    93  }
    94  
    95  func TestFrontendBasicWorkflow(t *testing.T) {
    96  	const (
    97  		body   = "all fine here"
    98  		userID = "test"
    99  	)
   100  
   101  	f, _ := setupFrontend(t, func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend {
   102  		// We cannot call QueryResult directly, as Frontend is not yet waiting for the response.
   103  		// It first needs to be told that enqueuing has succeeded.
   104  		go sendResponseWithDelay(f, 100*time.Millisecond, userID, msg.QueryID, &httpgrpc.HTTPResponse{
   105  			Code: 200,
   106  			Body: []byte(body),
   107  		})
   108  
   109  		return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK}
   110  	})
   111  
   112  	resp, err := f.RoundTripGRPC(user.InjectOrgID(context.Background(), userID), &httpgrpc.HTTPRequest{})
   113  	require.NoError(t, err)
   114  	require.Equal(t, int32(200), resp.Code)
   115  	require.Equal(t, []byte(body), resp.Body)
   116  }
   117  
   118  func TestFrontendRetryEnqueue(t *testing.T) {
   119  	// Frontend uses worker concurrency to compute number of retries. We use one less failure.
   120  	failures := atomic.NewInt64(testFrontendWorkerConcurrency - 1)
   121  	const (
   122  		body   = "hello world"
   123  		userID = "test"
   124  	)
   125  
   126  	f, _ := setupFrontend(t, func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend {
   127  		fail := failures.Dec()
   128  		if fail >= 0 {
   129  			return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SHUTTING_DOWN}
   130  		}
   131  
   132  		go sendResponseWithDelay(f, 100*time.Millisecond, userID, msg.QueryID, &httpgrpc.HTTPResponse{
   133  			Code: 200,
   134  			Body: []byte(body),
   135  		})
   136  
   137  		return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK}
   138  	})
   139  
   140  	_, err := f.RoundTripGRPC(user.InjectOrgID(context.Background(), userID), &httpgrpc.HTTPRequest{})
   141  	require.NoError(t, err)
   142  }
   143  
   144  func TestFrontendEnqueueFailure(t *testing.T) {
   145  	f, _ := setupFrontend(t, func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend {
   146  		return &schedulerpb.SchedulerToFrontend{Status: schedulerpb.SHUTTING_DOWN}
   147  	})
   148  
   149  	_, err := f.RoundTripGRPC(user.InjectOrgID(context.Background(), "test"), &httpgrpc.HTTPRequest{})
   150  	require.Error(t, err)
   151  	require.True(t, strings.Contains(err.Error(), "failed to enqueue request"))
   152  }
   153  
   154  func TestFrontendCancellation(t *testing.T) {
   155  	f, ms := setupFrontend(t, nil)
   156  
   157  	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
   158  	defer cancel()
   159  
   160  	resp, err := f.RoundTripGRPC(user.InjectOrgID(ctx, "test"), &httpgrpc.HTTPRequest{})
   161  	require.EqualError(t, err, context.DeadlineExceeded.Error())
   162  	require.Nil(t, resp)
   163  
   164  	// We wait a bit to make sure scheduler receives the cancellation request.
   165  	test.Poll(t, time.Second, 2, func() interface{} {
   166  		ms.mu.Lock()
   167  		defer ms.mu.Unlock()
   168  
   169  		return len(ms.msgs)
   170  	})
   171  
   172  	ms.checkWithLock(func() {
   173  		require.Equal(t, 2, len(ms.msgs))
   174  		require.True(t, ms.msgs[0].Type == schedulerpb.ENQUEUE)
   175  		require.True(t, ms.msgs[1].Type == schedulerpb.CANCEL)
   176  		require.True(t, ms.msgs[0].QueryID == ms.msgs[1].QueryID)
   177  	})
   178  }
   179  
   180  func TestFrontendFailedCancellation(t *testing.T) {
   181  	f, ms := setupFrontend(t, nil)
   182  
   183  	ctx, cancel := context.WithCancel(context.Background())
   184  	defer cancel()
   185  
   186  	go func() {
   187  		time.Sleep(100 * time.Millisecond)
   188  
   189  		// stop scheduler workers
   190  		addr := ""
   191  		f.schedulerWorkers.mu.Lock()
   192  		for k := range f.schedulerWorkers.workers {
   193  			addr = k
   194  			break
   195  		}
   196  		f.schedulerWorkers.mu.Unlock()
   197  
   198  		f.schedulerWorkers.AddressRemoved(addr)
   199  
   200  		// Wait for worker goroutines to stop.
   201  		time.Sleep(100 * time.Millisecond)
   202  
   203  		// Cancel request. Frontend will try to send cancellation to scheduler, but that will fail (not visible to user).
   204  		// Everything else should still work fine.
   205  		cancel()
   206  	}()
   207  
   208  	// send request
   209  	resp, err := f.RoundTripGRPC(user.InjectOrgID(ctx, "test"), &httpgrpc.HTTPRequest{})
   210  	require.EqualError(t, err, context.Canceled.Error())
   211  	require.Nil(t, resp)
   212  
   213  	ms.checkWithLock(func() {
   214  		require.Equal(t, 1, len(ms.msgs))
   215  	})
   216  }
   217  
   218  type mockScheduler struct {
   219  	t *testing.T
   220  	f *Frontend
   221  
   222  	replyFunc func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend
   223  
   224  	mu           sync.Mutex
   225  	frontendAddr map[string]int
   226  	msgs         []*schedulerpb.FrontendToScheduler
   227  }
   228  
   229  func newMockScheduler(t *testing.T, f *Frontend, replyFunc func(f *Frontend, msg *schedulerpb.FrontendToScheduler) *schedulerpb.SchedulerToFrontend) *mockScheduler {
   230  	return &mockScheduler{t: t, f: f, frontendAddr: map[string]int{}, replyFunc: replyFunc}
   231  }
   232  
   233  func (m *mockScheduler) checkWithLock(fn func()) {
   234  	m.mu.Lock()
   235  	defer m.mu.Unlock()
   236  
   237  	fn()
   238  }
   239  
   240  func (m *mockScheduler) FrontendLoop(frontend schedulerpb.SchedulerForFrontend_FrontendLoopServer) error {
   241  	init, err := frontend.Recv()
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	m.mu.Lock()
   247  	m.frontendAddr[init.FrontendAddress]++
   248  	m.mu.Unlock()
   249  
   250  	// Ack INIT from frontend.
   251  	if err := frontend.Send(&schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK}); err != nil {
   252  		return err
   253  	}
   254  
   255  	for {
   256  		msg, err := frontend.Recv()
   257  		if err != nil {
   258  			return err
   259  		}
   260  
   261  		m.mu.Lock()
   262  		m.msgs = append(m.msgs, msg)
   263  		m.mu.Unlock()
   264  
   265  		reply := &schedulerpb.SchedulerToFrontend{Status: schedulerpb.OK}
   266  		if m.replyFunc != nil {
   267  			reply = m.replyFunc(m.f, msg)
   268  		}
   269  
   270  		if err := frontend.Send(reply); err != nil {
   271  			return err
   272  		}
   273  	}
   274  }