github.com/grafana/pyroscope@v1.18.0/pkg/util/servicediscovery/ring_test.go (about) 1 // SPDX-License-Identifier: AGPL-3.0-only 2 3 package servicediscovery 4 5 import ( 6 "context" 7 "sort" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/go-kit/log" 13 "github.com/grafana/dskit/kv/consul" 14 "github.com/grafana/dskit/ring" 15 "github.com/grafana/dskit/services" 16 "github.com/grafana/dskit/test" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 ) 20 21 func TestRingServiceDiscovery_WithoutMaxUsedInstances(t *testing.T) { 22 const ( 23 ringKey = "test" 24 ringCheckPeriod = 100 * time.Millisecond // Check very frequently to speed up the test. 25 ) 26 27 // Use an in-memory KV store. 28 inmem, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil) 29 t.Cleanup(func() { _ = closer.Close() }) 30 31 // Create a ring client. 32 ringCfg := ring.Config{HeartbeatTimeout: time.Minute, ReplicationFactor: 1} 33 ringClient, err := ring.NewWithStoreClientAndStrategy(ringCfg, "test", ringKey, inmem, ring.NewDefaultReplicationStrategy(), nil, log.NewNopLogger()) 34 require.NoError(t, err) 35 36 // Create an empty ring. 37 ctx := context.Background() 38 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 39 return ring.NewDesc(), true, nil 40 })) 41 42 // Mock a receiver to keep track of all notified addresses. 43 receiver := newNotificationsReceiverMock() 44 45 sd := NewRing(ringClient, ringCheckPeriod, 0, receiver) 46 47 // Start the service discovery. 48 require.NoError(t, services.StartAndAwaitRunning(ctx, sd)) 49 t.Cleanup(func() { 50 require.NoError(t, services.StopAndAwaitTerminated(ctx, sd)) 51 }) 52 53 // Wait some time, we expect no address notified because the ring is empty. 54 time.Sleep(time.Second) 55 require.Empty(t, receiver.getDiscoveredInstances()) 56 57 // Register some instances. 58 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 59 desc := in.(*ring.Desc) 60 desc.AddIngester("instance-1", "127.0.0.1", "", nil, ring.ACTIVE, time.Now(), false, time.Now()) 61 desc.AddIngester("instance-2", "127.0.0.2", "", nil, ring.PENDING, time.Now(), false, time.Now()) 62 desc.AddIngester("instance-3", "127.0.0.3", "", nil, ring.JOINING, time.Now(), false, time.Now()) 63 desc.AddIngester("instance-4", "127.0.0.4", "", nil, ring.LEAVING, time.Now(), false, time.Now()) 64 return desc, true, nil 65 })) 66 67 test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}}, func() interface{} { 68 return receiver.getDiscoveredInstances() 69 }) 70 71 // Register more instances. 72 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 73 desc := in.(*ring.Desc) 74 desc.AddIngester("instance-5", "127.0.0.5", "", nil, ring.ACTIVE, time.Now(), false, time.Now()) 75 desc.AddIngester("instance-6", "127.0.0.6", "", nil, ring.ACTIVE, time.Now(), false, time.Now()) 76 return desc, true, nil 77 })) 78 79 test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}, {"127.0.0.5", true}, {"127.0.0.6", true}}, func() interface{} { 80 return receiver.getDiscoveredInstances() 81 }) 82 83 // Unregister some instances. 84 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 85 desc := in.(*ring.Desc) 86 desc.RemoveIngester("instance-1") 87 desc.RemoveIngester("instance-6") 88 return desc, true, nil 89 })) 90 91 test.Poll(t, time.Second, []Instance{{"127.0.0.5", true}}, func() interface{} { 92 return receiver.getDiscoveredInstances() 93 }) 94 95 // A non-active instance switches to active. 96 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 97 desc := in.(*ring.Desc) 98 instance := desc.Ingesters["instance-2"] 99 instance.State = ring.ACTIVE 100 desc.Ingesters["instance-2"] = instance 101 return desc, true, nil 102 })) 103 104 test.Poll(t, time.Second, []Instance{{"127.0.0.2", true}, {"127.0.0.5", true}}, func() interface{} { 105 return receiver.getDiscoveredInstances() 106 }) 107 108 // An active becomes unhealthy. 109 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 110 desc := in.(*ring.Desc) 111 instance := desc.Ingesters["instance-2"] 112 instance.Timestamp = time.Now().Add(-2 * ringCfg.HeartbeatTimeout).Unix() 113 desc.Ingesters["instance-2"] = instance 114 return desc, true, nil 115 })) 116 117 test.Poll(t, time.Second, []Instance{{"127.0.0.5", true}}, func() interface{} { 118 return receiver.getDiscoveredInstances() 119 }) 120 } 121 122 func TestRingServiceDiscovery_WithMaxUsedInstances(t *testing.T) { 123 const ( 124 ringKey = "test" 125 ringCheckPeriod = 100 * time.Millisecond // Check very frequently to speed up the test. 126 maxUsedInstances = 2 127 ) 128 129 // Use an in-memory KV store. 130 inmem, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil) 131 t.Cleanup(func() { _ = closer.Close() }) 132 133 // Create a ring client. 134 ringCfg := ring.Config{HeartbeatTimeout: time.Minute, ReplicationFactor: 1} 135 ringClient, err := ring.NewWithStoreClientAndStrategy(ringCfg, "test", ringKey, inmem, ring.NewDefaultReplicationStrategy(), nil, log.NewNopLogger()) 136 require.NoError(t, err) 137 138 // Create an empty ring. 139 ctx := context.Background() 140 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 141 return ring.NewDesc(), true, nil 142 })) 143 144 // Mock a receiver to keep track of all notified addresses. 145 receiver := newNotificationsReceiverMock() 146 147 sd := NewRing(ringClient, ringCheckPeriod, maxUsedInstances, receiver) 148 149 // Start the service discovery. 150 require.NoError(t, services.StartAndAwaitRunning(ctx, sd)) 151 t.Cleanup(func() { 152 require.NoError(t, services.StopAndAwaitTerminated(ctx, sd)) 153 }) 154 155 // Wait some time, we expect no address notified because the ring is empty. 156 time.Sleep(time.Second) 157 require.Empty(t, receiver.getDiscoveredInstances()) 158 159 // Register some instances. 160 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 161 desc := in.(*ring.Desc) 162 desc.AddIngester("instance-1", "127.0.0.1", "", nil, ring.ACTIVE, time.Now(), false, time.Now()) 163 desc.AddIngester("instance-2", "127.0.0.2", "", nil, ring.PENDING, time.Now(), false, time.Now()) 164 desc.AddIngester("instance-3", "127.0.0.3", "", nil, ring.JOINING, time.Now(), false, time.Now()) 165 desc.AddIngester("instance-4", "127.0.0.4", "", nil, ring.LEAVING, time.Now(), false, time.Now()) 166 return desc, true, nil 167 })) 168 169 test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}}, func() interface{} { 170 return receiver.getDiscoveredInstances() 171 }) 172 173 // Register more instances. 174 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 175 desc := in.(*ring.Desc) 176 desc.AddIngester("instance-5", "127.0.0.5", "", nil, ring.ACTIVE, time.Now(), false, time.Now()) 177 desc.AddIngester("instance-6", "127.0.0.6", "", nil, ring.ACTIVE, time.Now(), false, time.Now()) 178 return desc, true, nil 179 })) 180 181 test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}, {"127.0.0.5", true}, {"127.0.0.6", false}}, func() interface{} { 182 return receiver.getDiscoveredInstances() 183 }) 184 185 // Unregister some instances. 186 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 187 desc := in.(*ring.Desc) 188 desc.RemoveIngester("instance-1") 189 desc.RemoveIngester("instance-6") 190 return desc, true, nil 191 })) 192 193 test.Poll(t, time.Second, []Instance{{"127.0.0.5", true}}, func() interface{} { 194 return receiver.getDiscoveredInstances() 195 }) 196 197 // Some non-active instances switch to active. 198 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 199 desc := in.(*ring.Desc) 200 instance := desc.Ingesters["instance-2"] 201 instance.State = ring.ACTIVE 202 desc.Ingesters["instance-2"] = instance 203 204 instance = desc.Ingesters["instance-3"] 205 instance.State = ring.ACTIVE 206 desc.Ingesters["instance-3"] = instance 207 208 return desc, true, nil 209 })) 210 211 test.Poll(t, time.Second, []Instance{{"127.0.0.2", true}, {"127.0.0.3", true}, {"127.0.0.5", false}}, func() interface{} { 212 return receiver.getDiscoveredInstances() 213 }) 214 215 // An active becomes unhealthy. 216 require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) { 217 desc := in.(*ring.Desc) 218 instance := desc.Ingesters["instance-2"] 219 instance.Timestamp = time.Now().Add(-2 * ringCfg.HeartbeatTimeout).Unix() 220 desc.Ingesters["instance-2"] = instance 221 return desc, true, nil 222 })) 223 224 test.Poll(t, time.Second, []Instance{{"127.0.0.3", true}, {"127.0.0.5", true}}, func() interface{} { 225 return receiver.getDiscoveredInstances() 226 }) 227 } 228 229 func TestSelectInUseInstances(t *testing.T) { 230 tests := map[string]struct { 231 input []ring.InstanceDesc 232 maxInstances int 233 expected []ring.InstanceDesc 234 }{ 235 "should return the input on empty list of instances": { 236 input: nil, 237 maxInstances: 3, 238 expected: nil, 239 }, 240 "should return the input on a number of instances < max instances": { 241 input: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}}, 242 maxInstances: 3, 243 expected: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}}, 244 }, 245 "should return the input on a number of instances = max instances": { 246 input: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}}, 247 maxInstances: 3, 248 expected: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}}, 249 }, 250 "should return a subset of the input on a number of instances > max instances": { 251 input: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}, {Addr: "4.4.4.4"}, {Addr: "5.5.5.5"}}, 252 maxInstances: 3, 253 expected: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "2.2.2.2"}, {Addr: "3.3.3.3"}}, 254 }, 255 "should return the input if max instances is 0": { 256 input: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}}, 257 maxInstances: 0, 258 expected: []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}}, 259 }, 260 } 261 262 for testName, testData := range tests { 263 t.Run(testName, func(t *testing.T) { 264 assert.Equal(t, testData.expected, selectInUseInstances(testData.input, testData.maxInstances)) 265 }) 266 } 267 } 268 269 type notificationsReceiverMock struct { 270 discoveredInstancesMx sync.Mutex 271 discoveredInstances map[string]Instance 272 } 273 274 func newNotificationsReceiverMock() *notificationsReceiverMock { 275 return ¬ificationsReceiverMock{ 276 discoveredInstances: map[string]Instance{}, 277 } 278 } 279 280 func (r *notificationsReceiverMock) InstanceAdded(instance Instance) { 281 r.discoveredInstancesMx.Lock() 282 defer r.discoveredInstancesMx.Unlock() 283 284 r.discoveredInstances[instance.Address] = instance 285 } 286 287 func (r *notificationsReceiverMock) InstanceRemoved(instance Instance) { 288 r.discoveredInstancesMx.Lock() 289 defer r.discoveredInstancesMx.Unlock() 290 291 delete(r.discoveredInstances, instance.Address) 292 } 293 294 func (r *notificationsReceiverMock) InstanceChanged(instance Instance) { 295 r.discoveredInstancesMx.Lock() 296 defer r.discoveredInstancesMx.Unlock() 297 298 r.discoveredInstances[instance.Address] = instance 299 } 300 301 func (r *notificationsReceiverMock) getDiscoveredInstances() []Instance { 302 r.discoveredInstancesMx.Lock() 303 defer r.discoveredInstancesMx.Unlock() 304 305 out := make([]Instance, 0, len(r.discoveredInstances)) 306 for _, instance := range r.discoveredInstances { 307 out = append(out, instance) 308 } 309 310 sort.Slice(out, func(i, j int) bool { 311 return out[i].Address < out[j].Address 312 }) 313 314 return out 315 }