Skip to content

Commit e35ea4f

Browse files
authored
Add readiness check for distributor (#5142)
* add readiness check for distributor * Add more tests * Switch v2 run configuration to be v2-only and not combined
1 parent 9c19be8 commit e35ea4f

7 files changed

Lines changed: 250 additions & 9 deletions

File tree

.idea/runConfigurations/v2.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/distributor/distributor.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ type PushClient interface {
6161
Push(context.Context, *connect.Request[pushv1.PushRequest]) (*connect.Response[pushv1.PushResponse], error)
6262
}
6363

64+
type SegmentWriterClient interface {
65+
Push(context.Context, *segmentwriterv1.PushRequest) (*segmentwriterv1.PushResponse, error)
66+
CheckReady(context.Context) error
67+
}
68+
6469
const (
6570
// distributorRingKey is the key under which we store the distributors ring in the KVStore.
6671
distributorRingKey = "distributor"
@@ -121,7 +126,7 @@ type Distributor struct {
121126
profileSizeStats *usagestats.MultiStatistics
122127

123128
router *writepath.Router
124-
segmentWriter writepath.SegmentWriterClient
129+
segmentWriter SegmentWriterClient
125130
}
126131

127132
type Limits interface {
@@ -157,7 +162,7 @@ func New(
157162
limits Limits,
158163
reg prometheus.Registerer,
159164
logger log.Logger,
160-
segmentWriter writepath.SegmentWriterClient,
165+
segmentWriter SegmentWriterClient,
161166
ingesterClientsOptions ...connect.ClientOption,
162167
) (*Distributor, error) {
163168
ingesterClientsOptions = append(
@@ -240,6 +245,54 @@ func (d *Distributor) stopping(_ error) error {
240245
return services.StopManagerAndAwaitStopped(context.Background(), d.subservices)
241246
}
242247

248+
// CheckReady reports whether the distributor is ready to serve requests.
249+
// It verifies the destinations selected by the deployment default write path,
250+
// so the distributor does not accept traffic before the relevant ring is
251+
// populated during rollouts.
252+
func (d *Distributor) CheckReady(ctx context.Context) error {
253+
if s := d.State(); s != services.Running && s != services.Stopping {
254+
return fmt.Errorf("distributor not ready: %v", s)
255+
}
256+
257+
switch d.limits.WritePathOverrides("").WritePath {
258+
case writepath.SegmentWriterPath:
259+
return d.checkSegmentWriterReady(ctx)
260+
case writepath.CombinedPath:
261+
if err := d.checkIngesterRingReady(ctx); err != nil {
262+
return fmt.Errorf("ingester write path: %w", err)
263+
}
264+
if err := d.checkSegmentWriterReady(ctx); err != nil {
265+
return fmt.Errorf("segment-writer write path: %w", err)
266+
}
267+
return nil
268+
default:
269+
return d.checkIngesterRingReady(ctx)
270+
}
271+
}
272+
273+
// checkIngesterRingReady reports whether the ingester ring has at least one
274+
// healthy instance available for writes.
275+
func (d *Distributor) checkIngesterRingReady(context.Context) error {
276+
if d.ingestersRing == nil {
277+
return errors.New("ingester ring not configured")
278+
}
279+
rs, err := d.ingestersRing.GetAllHealthy(ring.Write)
280+
if err != nil {
281+
return fmt.Errorf("ingester ring: %w", err)
282+
}
283+
if len(rs.Instances) == 0 {
284+
return errors.New("ingester ring has no healthy instances")
285+
}
286+
return nil
287+
}
288+
289+
func (d *Distributor) checkSegmentWriterReady(ctx context.Context) error {
290+
if d.segmentWriter == nil {
291+
return errors.New("segment-writer client not configured")
292+
}
293+
return d.segmentWriter.CheckReady(ctx)
294+
}
295+
243296
func isKnownConnectError(err error) bool {
244297
ce := new(connect.Error)
245298
if !errors.As(err, &ce) {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package distributor
2+
3+
import (
4+
"context"
5+
"flag"
6+
"os"
7+
"testing"
8+
"time"
9+
10+
"github.com/go-kit/log"
11+
"github.com/grafana/dskit/grpcclient"
12+
"github.com/grafana/dskit/ring"
13+
"github.com/grafana/dskit/ring/client"
14+
"github.com/grafana/dskit/services"
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/grafana/pyroscope/v2/pkg/clientpool"
18+
"github.com/grafana/pyroscope/v2/pkg/distributor/writepath"
19+
segmentwriterclient "github.com/grafana/pyroscope/v2/pkg/segmentwriter/client"
20+
"github.com/grafana/pyroscope/v2/pkg/testhelper"
21+
"github.com/grafana/pyroscope/v2/pkg/validation"
22+
)
23+
24+
func newReadinessSegmentWriterClient(t *testing.T, ctx context.Context, logger log.Logger, r ring.ReadRing) *segmentwriterclient.Client {
25+
t.Helper()
26+
var grpcCfg grpcclient.Config
27+
grpcCfg.RegisterFlags(flag.NewFlagSet("", flag.PanicOnError))
28+
29+
swClient, err := segmentwriterclient.NewSegmentWriterClient(
30+
grpcCfg, logger, nil, r, nil,
31+
)
32+
require.NoError(t, err)
33+
require.NoError(t, services.StartAndAwaitRunning(ctx, swClient.Service()))
34+
t.Cleanup(func() {
35+
_ = services.StopAndAwaitTerminated(context.Background(), swClient.Service())
36+
})
37+
return swClient
38+
}
39+
40+
func newReadinessDistributor(
41+
t *testing.T,
42+
ctx context.Context,
43+
logger log.Logger,
44+
path writepath.WritePath,
45+
ingesterRing ring.ReadRing,
46+
swClient SegmentWriterClient,
47+
) *Distributor {
48+
t.Helper()
49+
overrides := validation.MockOverrides(func(defaults *validation.Limits, _ map[string]*validation.Limits) {
50+
defaults.WritePathOverrides.WritePath = path
51+
})
52+
53+
d, err := New(
54+
Config{
55+
DistributorRing: ringConfig,
56+
PoolConfig: clientpool.PoolConfig{ClientCleanupPeriod: time.Second},
57+
},
58+
ingesterRing,
59+
&poolFactory{f: func(string) (client.PoolClient, error) {
60+
return newFakeIngester(t, false), nil
61+
}},
62+
overrides,
63+
nil,
64+
logger,
65+
swClient,
66+
)
67+
require.NoError(t, err)
68+
require.NoError(t, services.StartAndAwaitRunning(ctx, d))
69+
t.Cleanup(func() {
70+
_ = services.StopAndAwaitTerminated(context.Background(), d)
71+
})
72+
require.Equal(t, services.Running, d.State())
73+
return d
74+
}
75+
76+
func TestDistributor_CheckReady_V2SegmentWriterPath(t *testing.T) {
77+
logger := log.NewLogfmtLogger(os.Stdout)
78+
ctx := context.Background()
79+
80+
tests := []struct {
81+
name string
82+
segmentWriterRing ring.ReadRing
83+
wantErr bool
84+
}{
85+
{
86+
name: "ready when segment-writer ring has healthy instances",
87+
segmentWriterRing: testhelper.NewMockRing([]ring.InstanceDesc{{Addr: "foo"}}, 1),
88+
},
89+
{
90+
name: "not ready when segment-writer ring is empty",
91+
segmentWriterRing: testhelper.NewMockRing(nil, 1),
92+
wantErr: true,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
swClient := newReadinessSegmentWriterClient(t, ctx, logger, tt.segmentWriterRing)
99+
d := newReadinessDistributor(
100+
t,
101+
ctx,
102+
logger,
103+
writepath.SegmentWriterPath,
104+
testhelper.NewMockRing([]ring.InstanceDesc{{Addr: "foo"}}, 3),
105+
swClient,
106+
)
107+
108+
err := d.CheckReady(ctx)
109+
if tt.wantErr {
110+
require.Error(t, err)
111+
return
112+
}
113+
require.NoError(t, err)
114+
})
115+
}
116+
}
117+
118+
func TestDistributor_CheckReady_V1IngesterPath(t *testing.T) {
119+
logger := log.NewLogfmtLogger(os.Stdout)
120+
ctx := context.Background()
121+
122+
tests := []struct {
123+
name string
124+
ingesterRing ring.ReadRing
125+
wantErr bool
126+
}{
127+
{
128+
name: "ready when ingester ring has healthy instances",
129+
ingesterRing: testhelper.NewMockRing([]ring.InstanceDesc{{Addr: "foo"}}, 3),
130+
},
131+
{
132+
name: "not ready when ingester ring is empty",
133+
ingesterRing: testhelper.NewMockRing(nil, 3),
134+
wantErr: true,
135+
},
136+
}
137+
138+
for _, tt := range tests {
139+
t.Run(tt.name, func(t *testing.T) {
140+
d := newReadinessDistributor(
141+
t,
142+
ctx,
143+
logger,
144+
writepath.IngesterPath,
145+
tt.ingesterRing,
146+
nil,
147+
)
148+
149+
err := d.CheckReady(ctx)
150+
if tt.wantErr {
151+
require.Error(t, err)
152+
return
153+
}
154+
require.NoError(t, err)
155+
})
156+
}
157+
}

pkg/distributor/distributor_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import (
4646
pprof2 "github.com/grafana/pyroscope/v2/pkg/pprof"
4747
pproftesthelper "github.com/grafana/pyroscope/v2/pkg/pprof/testhelper"
4848
"github.com/grafana/pyroscope/v2/pkg/tenant"
49-
"github.com/grafana/pyroscope/v2/pkg/test/mocks/mockwritepath"
5049
"github.com/grafana/pyroscope/v2/pkg/testhelper"
5150
"github.com/grafana/pyroscope/v2/pkg/util"
5251
"github.com/grafana/pyroscope/v2/pkg/validation"
@@ -1795,7 +1794,7 @@ func Test_SampleLabels_SegmentWriter(t *testing.T) {
17951794
{Addr: "foo"},
17961795
}, 3), &poolFactory{func(addr string) (client.PoolClient, error) {
17971796
return newFakeIngester(t, false), nil
1798-
}}, overrides, nil, log.NewLogfmtLogger(os.Stdout), new(mockwritepath.MockSegmentWriterClient))
1797+
}}, overrides, nil, log.NewLogfmtLogger(os.Stdout), nil)
17991798

18001799
require.NoError(t, err)
18011800
var series []*distributormodel.ProfileSeries

pkg/pyroscope/modules.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"golang.org/x/net/http2"
3535
"golang.org/x/net/http2/h2c"
3636

37-
statusv1 "github.com/grafana/pyroscope/api/gen/proto/go/status/v1"
3837
"github.com/grafana/pyroscope/v2/pkg/adhocprofiles"
3938
apiversion "github.com/grafana/pyroscope/v2/pkg/api/version"
4039
"github.com/grafana/pyroscope/v2/pkg/compactor"
@@ -60,6 +59,8 @@ import (
6059
httputil "github.com/grafana/pyroscope/v2/pkg/util/http"
6160
"github.com/grafana/pyroscope/v2/pkg/validation"
6261
"github.com/grafana/pyroscope/v2/pkg/validation/exporter"
62+
63+
statusv1 "github.com/grafana/pyroscope/api/gen/proto/go/status/v1"
6364
)
6465

6566
// The various modules that make up Pyroscope.
@@ -322,10 +323,15 @@ func (f *Pyroscope) initGRPCGateway() (services.Service, error) {
322323
func (f *Pyroscope) initDistributor() (services.Service, error) {
323324
f.Cfg.Distributor.DistributorRing.ListenPort = f.Cfg.Server.HTTPListenPort
324325
logger := log.With(f.logger, "component", "distributor")
325-
d, err := distributor.New(f.Cfg.Distributor, f.ingesterRing, nil, f.Overrides, f.reg, logger, f.segmentWriterClient, f.auth)
326+
var swClient distributor.SegmentWriterClient
327+
if f.segmentWriterClient != nil {
328+
swClient = f.segmentWriterClient
329+
}
330+
d, err := distributor.New(f.Cfg.Distributor, f.ingesterRing, nil, f.Overrides, f.reg, logger, swClient, f.auth)
326331
if err != nil {
327332
return nil, err
328333
}
334+
f.distributor = d
329335
f.API.RegisterDistributor(d, f.Overrides, f.Cfg.Server)
330336

331337
if store, err := debuginfo.NewStore(f.logger, f.storageBucket, f.Cfg.DebugInfo); err != nil {

pkg/pyroscope/pyroscope.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,9 @@ type Pyroscope struct {
392392

393393
grpcGatewayMux *grpcgw.ServeMux
394394

395-
auth connect.Option
396-
frontend *frontend.Frontend
395+
auth connect.Option
396+
frontend *frontend.Frontend
397+
distributor *distributor.Distributor
397398

398399
segmentWriter *segmentwriter.SegmentWriterService
399400
segmentWriterClient *segmentwriterclient.Client
@@ -788,6 +789,13 @@ func (f *Pyroscope) readyHandler(sm *services.Manager) http.HandlerFunc {
788789
}
789790
}
790791

792+
if f.distributor != nil {
793+
if err := f.distributor.CheckReady(r.Context()); err != nil {
794+
http.Error(w, "Distributor not ready: "+err.Error(), http.StatusServiceUnavailable)
795+
return
796+
}
797+
}
798+
791799
util.WriteTextResponse(w, "ready")
792800
}
793801
}

pkg/segmentwriter/client/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,24 @@ func NewSegmentWriterClient(
183183

184184
func (c *Client) Service() services.Service { return c.service }
185185

186+
// CheckReady reports whether the client can dispatch requests, i.e. its
187+
// service is Running and the segment-writer ring has at least one healthy
188+
// instance. Used by callers (e.g. the distributor) to gate readiness so
189+
// that traffic isn't accepted before the ring has been populated.
190+
func (c *Client) CheckReady(_ context.Context) error {
191+
if state := c.service.State(); state != services.Running {
192+
return fmt.Errorf("segment-writer client not running: %s", state)
193+
}
194+
rs, err := c.ring.GetAllHealthy(ring.Reporting)
195+
if err != nil {
196+
return fmt.Errorf("segment-writer ring: %w", err)
197+
}
198+
if len(rs.Instances) == 0 {
199+
return errors.New("segment-writer ring has no healthy instances")
200+
}
201+
return nil
202+
}
203+
186204
func (c *Client) starting(ctx context.Context) error {
187205
// Warm up connections. The pool does not do this.
188206
instances, err := c.ring.GetAllHealthy(ring.Reporting)

0 commit comments

Comments
 (0)