github.com/grafana/pyroscope@v1.18.0/pkg/api/version/version_test.go (about)

     1  package version
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"connectrpc.com/connect"
    10  	"github.com/go-kit/log"
    11  	"github.com/grafana/dskit/flagext"
    12  	"github.com/grafana/dskit/kv"
    13  	"github.com/grafana/dskit/kv/codec"
    14  	"github.com/grafana/dskit/kv/memberlist"
    15  	"github.com/grafana/dskit/services"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  
    20  	versionv1 "github.com/grafana/pyroscope/api/gen/proto/go/version/v1"
    21  	"github.com/grafana/pyroscope/pkg/util"
    22  )
    23  
    24  type dnsProviderMock struct {
    25  	resolved []string
    26  }
    27  
    28  func (p *dnsProviderMock) Resolve(ctx context.Context, addrs []string) error {
    29  	p.resolved = addrs
    30  	return nil
    31  }
    32  
    33  func (p dnsProviderMock) Addresses() []string {
    34  	return p.resolved
    35  }
    36  
    37  func createMemberlist(t *testing.T, port, memberID int) *memberlist.KV {
    38  	t.Helper()
    39  	var cfg memberlist.KVConfig
    40  	flagext.DefaultValues(&cfg)
    41  	cfg.TCPTransport = memberlist.TCPTransportConfig{
    42  		BindAddrs: []string{"127.0.0.1"},
    43  		BindPort:  0,
    44  	}
    45  	cfg.GossipInterval = 10 * time.Millisecond
    46  	cfg.GossipNodes = 4
    47  	cfg.PushPullInterval = 10 * time.Millisecond
    48  	cfg.NodeName = fmt.Sprintf("Member-%d", memberID)
    49  	cfg.Codecs = []codec.Codec{GetCodec()}
    50  
    51  	mkv := memberlist.NewKV(cfg, log.NewNopLogger(), &dnsProviderMock{}, nil)
    52  	require.NoError(t, services.StartAndAwaitRunning(context.Background(), mkv))
    53  	if port != 0 {
    54  		_, err := mkv.JoinMembers([]string{fmt.Sprintf("127.0.0.1:%d", port)})
    55  		require.NoError(t, err, "%s failed to join the cluster: %v", memberID, err)
    56  	}
    57  	t.Cleanup(func() {
    58  		_ = services.StopAndAwaitTerminated(context.TODO(), mkv)
    59  	})
    60  	return mkv
    61  }
    62  
    63  func setupTests(t *testing.T) int {
    64  	t.Helper()
    65  	heartbeatInterval = 100 * time.Millisecond
    66  	instanceTimeout = 500 * time.Millisecond
    67  	initMKV := createMemberlist(t, 0, 0)
    68  	return initMKV.GetListeningPort()
    69  }
    70  
    71  func TestVersionsSingle(t *testing.T) {
    72  	var (
    73  		port = setupTests(t)
    74  		ctx  = context.Background()
    75  		req  = &connect.Request[versionv1.VersionRequest]{}
    76  	)
    77  
    78  	svc, err := New(util.CommonRingConfig{
    79  		InstanceID:   "1",
    80  		InstanceAddr: "0.0.0.0",
    81  		InstancePort: 1,
    82  		KVStore: kv.Config{
    83  			Store: "memberlist",
    84  			StoreConfig: kv.StoreConfig{
    85  				MemberlistKV: func() (*memberlist.KV, error) {
    86  					return createMemberlist(t, port, 1), nil
    87  				},
    88  			},
    89  		},
    90  	}, log.NewNopLogger(), prometheus.NewRegistry())
    91  
    92  	require.NoError(t, err)
    93  
    94  	resp, err := svc.Version(ctx, req)
    95  	require.NoError(t, err)
    96  	require.Equal(t, uint64(0), resp.Msg.QuerierAPI)
    97  
    98  	// start the service
    99  	require.NoError(t, services.StartAndAwaitRunning(ctx, svc))
   100  
   101  	require.EventuallyWithT(t, func(t *assert.CollectT) {
   102  		resp, err := svc.Version(ctx, req)
   103  		assert.NoError(t, err)
   104  		assert.Equal(t, uint64(1), resp.Msg.QuerierAPI)
   105  	}, 1*time.Second, 500*time.Millisecond)
   106  
   107  	require.NoError(t, services.StopAndAwaitTerminated(ctx, svc))
   108  	svc.Shutdown()
   109  }
   110  
   111  func TestVersionsMultiple(t *testing.T) {
   112  	var (
   113  		port = setupTests(t)
   114  		ctx  = context.Background()
   115  		req  = &connect.Request[versionv1.VersionRequest]{}
   116  	)
   117  
   118  	svcs := make([]*Service, 0, 3)
   119  	for i := 0; i < 3; i++ {
   120  		svc, err := New(util.CommonRingConfig{
   121  			InstanceID: fmt.Sprintf("%d", i),
   122  			KVStore: kv.Config{
   123  				Store: "memberlist",
   124  				StoreConfig: kv.StoreConfig{
   125  					MemberlistKV: func() (*memberlist.KV, error) {
   126  						return createMemberlist(t, port, i), nil
   127  					},
   128  				},
   129  			},
   130  		}, log.NewNopLogger(), prometheus.NewRegistry())
   131  		require.NoError(t, err)
   132  		svcs = append(svcs, svc)
   133  	}
   134  	svcs[0].version = 1
   135  	svcs[1].version = 2
   136  	svcs[2].version = 2
   137  
   138  	expectVersion := func(t *testing.T, expected uint64) {
   139  		t.Helper()
   140  		require.EventuallyWithT(t, func(t *assert.CollectT) {
   141  			resp, err := svcs[0].Version(ctx, req)
   142  			assert.NoError(t, err)
   143  			assert.Equal(t, expected, resp.Msg.QuerierAPI)
   144  		}, 3*time.Second, 100*time.Millisecond)
   145  	}
   146  
   147  	expectVersion(t, 0)
   148  
   149  	require.NoError(t, services.StartAndAwaitRunning(ctx, svcs[0]))
   150  	expectVersion(t, 1)
   151  	require.NoError(t, services.StartAndAwaitRunning(ctx, svcs[1]))
   152  	expectVersion(t, 1)
   153  	require.NoError(t, services.StartAndAwaitRunning(ctx, svcs[2]))
   154  	expectVersion(t, 1)
   155  	// wait for the version to be propagated
   156  	svcs[0].Shutdown()
   157  	expectVersion(t, 2)
   158  }
   159  
   160  var nowTs = time.Now().UnixNano()
   161  
   162  func TestMerge(t *testing.T) {
   163  	now = func() time.Time {
   164  		return time.Unix(0, nowTs)
   165  	}
   166  	t.Cleanup(func() {
   167  		now = time.Now
   168  	})
   169  
   170  	for name, tc := range map[string]struct {
   171  		base     *Versions
   172  		incoming memberlist.Mergeable
   173  
   174  		expected memberlist.Mergeable
   175  	}{
   176  		"empty": {
   177  			base:     nil,
   178  			incoming: nil,
   179  			expected: nil,
   180  		},
   181  		"empty base": {
   182  			base:     nil,
   183  			incoming: &Versions{},
   184  			expected: &Versions{},
   185  		},
   186  		"empty incoming": {
   187  			base:     &Versions{},
   188  			incoming: nil,
   189  			expected: nil,
   190  		},
   191  		"equal": {
   192  			base: createVersions(t,
   193  				createVersion(t, "1", 1),
   194  				createVersion(t, "2", 2),
   195  			),
   196  			incoming: createVersions(t,
   197  				createVersion(t, "1", 1),
   198  				createVersion(t, "2", 2),
   199  			),
   200  			expected: nil,
   201  		},
   202  		"newer": {
   203  			base: createVersions(t,
   204  				createVersion(t, "1", 1),
   205  				createVersion(t, "2", 2),
   206  			),
   207  			incoming: createVersions(t,
   208  				createVersion(t, "1", 1),
   209  				createVersion(t, "2", 3),
   210  			),
   211  			expected: createVersions(t,
   212  				createVersion(t, "2", 3),
   213  			),
   214  		},
   215  		"instance added": {
   216  			base: createVersions(t,
   217  				createVersion(t, "1", 1),
   218  			),
   219  			incoming: createVersions(t,
   220  				createVersion(t, "1", 1),
   221  				createVersion(t, "2", 2),
   222  			),
   223  			expected: createVersions(t,
   224  				createVersion(t, "2", 2),
   225  			),
   226  		},
   227  		"instance removed": {
   228  			base: createVersions(t,
   229  				createVersion(t, "1", 1),
   230  				createVersion(t, "2", 2),
   231  			),
   232  			incoming: createVersions(t,
   233  				createVersion(t, "1", 1),
   234  			),
   235  			expected: createVersions(t,
   236  				createLeftVersion(t, "2", nowTs),
   237  			),
   238  		},
   239  		"instance removed and added": {
   240  			base: createVersions(t,
   241  				createVersion(t, "1", 1),
   242  				createVersion(t, "2", 2),
   243  			),
   244  			incoming: createVersions(t,
   245  				createVersion(t, "3", 3),
   246  				createVersion(t, "2", 2),
   247  			),
   248  			expected: createVersions(t,
   249  				createVersion(t, "3", 3),
   250  				createLeftVersion(t, "1", nowTs),
   251  			),
   252  		},
   253  	} {
   254  		tc := tc
   255  		t.Run(name, func(t *testing.T) {
   256  			t.Parallel()
   257  			change, err := tc.base.Merge(tc.incoming, true)
   258  			require.NoError(t, err)
   259  			require.Equal(t, tc.expected, change)
   260  		})
   261  	}
   262  }
   263  
   264  func createLeftVersion(t *testing.T, id string, ts int64) *versionv1.InstanceVersion {
   265  	t.Helper()
   266  	return &versionv1.InstanceVersion{
   267  		ID:        id,
   268  		Timestamp: ts,
   269  		Left:      true,
   270  	}
   271  }
   272  
   273  func createVersion(t *testing.T, id string, ts int64) *versionv1.InstanceVersion {
   274  	t.Helper()
   275  	return &versionv1.InstanceVersion{
   276  		ID:        id,
   277  		Timestamp: ts,
   278  	}
   279  }
   280  
   281  func createVersions(t *testing.T, instances ...*versionv1.InstanceVersion) *Versions {
   282  	t.Helper()
   283  	res := &Versions{
   284  		Versions: &versionv1.Versions{
   285  			Instances: map[string]*versionv1.InstanceVersion{},
   286  		},
   287  	}
   288  	for _, inst := range instances {
   289  		res.Instances[inst.ID] = inst
   290  	}
   291  	return res
   292  }