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 }