diff --git a/src/cmd/services/m3query/config/config.go b/src/cmd/services/m3query/config/config.go index 4e6c3925a0..028bd706b0 100644 --- a/src/cmd/services/m3query/config/config.go +++ b/src/cmd/services/m3query/config/config.go @@ -293,6 +293,9 @@ type PerQueryLimitsConfiguration struct { // MaxFetchedSeries limits the number of time series returned by a storage node. MaxFetchedSeries int `yaml:"maxFetchedSeries"` + + // RequireExhaustive results in an error if the query exceeds the series limit. + RequireExhaustive bool `yaml:"requireExhaustive"` } // AsLimitManagerOptions converts this configuration to @@ -306,12 +309,14 @@ func (l *PerQueryLimitsConfiguration) AsLimitManagerOptions() cost.LimitManagerO func (l *PerQueryLimitsConfiguration) AsFetchOptionsBuilderOptions() handleroptions.FetchOptionsBuilderOptions { if l.MaxFetchedSeries <= 0 { return handleroptions.FetchOptionsBuilderOptions{ - Limit: defaultStorageQueryLimit, + Limit: defaultStorageQueryLimit, + RequireExhaustive: l.RequireExhaustive, } } return handleroptions.FetchOptionsBuilderOptions{ - Limit: int(l.MaxFetchedSeries), + Limit: int(l.MaxFetchedSeries), + RequireExhaustive: l.RequireExhaustive, } } diff --git a/src/dbnode/generated/thrift/rpc.thrift b/src/dbnode/generated/thrift/rpc.thrift index 99adbfbf5d..709309d689 100644 --- a/src/dbnode/generated/thrift/rpc.thrift +++ b/src/dbnode/generated/thrift/rpc.thrift @@ -168,6 +168,7 @@ struct FetchTaggedRequest { 5: required bool fetchData 6: optional i64 limit 7: optional TimeType rangeTimeType = TimeType.UNIX_SECONDS + 8: optional bool requireExhaustive = false } struct FetchTaggedResult { diff --git a/src/dbnode/generated/thrift/rpc/rpc.go b/src/dbnode/generated/thrift/rpc/rpc.go index bff120a62e..da282734f3 100644 --- a/src/dbnode/generated/thrift/rpc/rpc.go +++ b/src/dbnode/generated/thrift/rpc/rpc.go @@ -3049,14 +3049,16 @@ func (p *Segment) String() string { // - FetchData // - Limit // - RangeTimeType +// - RequireExhaustive type FetchTaggedRequest struct { - NameSpace []byte `thrift:"nameSpace,1,required" db:"nameSpace" json:"nameSpace"` - Query []byte `thrift:"query,2,required" db:"query" json:"query"` - RangeStart int64 `thrift:"rangeStart,3,required" db:"rangeStart" json:"rangeStart"` - RangeEnd int64 `thrift:"rangeEnd,4,required" db:"rangeEnd" json:"rangeEnd"` - FetchData bool `thrift:"fetchData,5,required" db:"fetchData" json:"fetchData"` - Limit *int64 `thrift:"limit,6" db:"limit" json:"limit,omitempty"` - RangeTimeType TimeType `thrift:"rangeTimeType,7" db:"rangeTimeType" json:"rangeTimeType,omitempty"` + NameSpace []byte `thrift:"nameSpace,1,required" db:"nameSpace" json:"nameSpace"` + Query []byte `thrift:"query,2,required" db:"query" json:"query"` + RangeStart int64 `thrift:"rangeStart,3,required" db:"rangeStart" json:"rangeStart"` + RangeEnd int64 `thrift:"rangeEnd,4,required" db:"rangeEnd" json:"rangeEnd"` + FetchData bool `thrift:"fetchData,5,required" db:"fetchData" json:"fetchData"` + Limit *int64 `thrift:"limit,6" db:"limit" json:"limit,omitempty"` + RangeTimeType TimeType `thrift:"rangeTimeType,7" db:"rangeTimeType" json:"rangeTimeType,omitempty"` + RequireExhaustive bool `thrift:"requireExhaustive,8" db:"requireExhaustive" json:"requireExhaustive,omitempty"` } func NewFetchTaggedRequest() *FetchTaggedRequest { @@ -3099,6 +3101,12 @@ var FetchTaggedRequest_RangeTimeType_DEFAULT TimeType = 0 func (p *FetchTaggedRequest) GetRangeTimeType() TimeType { return p.RangeTimeType } + +var FetchTaggedRequest_RequireExhaustive_DEFAULT bool = false + +func (p *FetchTaggedRequest) GetRequireExhaustive() bool { + return p.RequireExhaustive +} func (p *FetchTaggedRequest) IsSetLimit() bool { return p.Limit != nil } @@ -3107,6 +3115,10 @@ func (p *FetchTaggedRequest) IsSetRangeTimeType() bool { return p.RangeTimeType != FetchTaggedRequest_RangeTimeType_DEFAULT } +func (p *FetchTaggedRequest) IsSetRequireExhaustive() bool { + return p.RequireExhaustive != FetchTaggedRequest_RequireExhaustive_DEFAULT +} + func (p *FetchTaggedRequest) Read(iprot thrift.TProtocol) error { if _, err := iprot.ReadStructBegin(); err != nil { return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) @@ -3160,6 +3172,10 @@ func (p *FetchTaggedRequest) Read(iprot thrift.TProtocol) error { if err := p.ReadField7(iprot); err != nil { return err } + case 8: + if err := p.ReadField8(iprot); err != nil { + return err + } default: if err := iprot.Skip(fieldTypeId); err != nil { return err @@ -3254,6 +3270,15 @@ func (p *FetchTaggedRequest) ReadField7(iprot thrift.TProtocol) error { return nil } +func (p *FetchTaggedRequest) ReadField8(iprot thrift.TProtocol) error { + if v, err := iprot.ReadBool(); err != nil { + return thrift.PrependError("error reading field 8: ", err) + } else { + p.RequireExhaustive = v + } + return nil +} + func (p *FetchTaggedRequest) Write(oprot thrift.TProtocol) error { if err := oprot.WriteStructBegin("FetchTaggedRequest"); err != nil { return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) @@ -3280,6 +3305,9 @@ func (p *FetchTaggedRequest) Write(oprot thrift.TProtocol) error { if err := p.writeField7(oprot); err != nil { return err } + if err := p.writeField8(oprot); err != nil { + return err + } } if err := oprot.WriteFieldStop(); err != nil { return thrift.PrependError("write field stop error: ", err) @@ -3385,6 +3413,21 @@ func (p *FetchTaggedRequest) writeField7(oprot thrift.TProtocol) (err error) { return err } +func (p *FetchTaggedRequest) writeField8(oprot thrift.TProtocol) (err error) { + if p.IsSetRequireExhaustive() { + if err := oprot.WriteFieldBegin("requireExhaustive", thrift.BOOL, 8); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field begin error 8:requireExhaustive: ", p), err) + } + if err := oprot.WriteBool(bool(p.RequireExhaustive)); err != nil { + return thrift.PrependError(fmt.Sprintf("%T.requireExhaustive (8) field write error: ", p), err) + } + if err := oprot.WriteFieldEnd(); err != nil { + return thrift.PrependError(fmt.Sprintf("%T write field end error 8:requireExhaustive: ", p), err) + } + } + return err +} + func (p *FetchTaggedRequest) String() string { if p == nil { return "" diff --git a/src/dbnode/storage/index.go b/src/dbnode/storage/index.go index 73d0700a9e..4f0fbc3d91 100644 --- a/src/dbnode/storage/index.go +++ b/src/dbnode/storage/index.go @@ -1210,9 +1210,32 @@ func (i *nsIndex) query( exhaustive, err := i.queryWithSpan(ctx, query, results, opts, execBlockFn, sp, logFields) if err != nil { sp.LogFields(opentracinglog.Error(err)) + + if exhaustive { + i.metrics.queryExhaustiveInternalError.Inc(1) + } else { + i.metrics.queryNonExhaustiveInternalError.Inc(1) + } + return exhaustive, err + } + + if exhaustive { + i.metrics.queryExhaustiveSuccess.Inc(1) + return exhaustive, nil } - return exhaustive, err + // If require exhaustive but not, return error. + if opts.RequireExhaustive { + i.metrics.queryNonExhaustiveLimitError.Inc(1) + return exhaustive, fmt.Errorf("query matched too many time series: require_exhaustive=%v, limit=%d, matched=%d", + opts.RequireExhaustive, + opts.Limit, + results.Size()) + } + + // Otherwise non-exhaustive but not required to be. + i.metrics.queryNonExhaustiveSuccess.Inc(1) + return exhaustive, nil } func (i *nsIndex) queryWithSpan( @@ -1370,7 +1393,6 @@ func (i *nsIndex) queryWithSpan( if err != nil { return false, err } - return exhaustive, nil } @@ -1847,7 +1869,12 @@ type nsIndexMetrics struct { blocksEvictedMutableSegments tally.Counter blockMetrics nsIndexBlocksMetrics - loadedDocsPerQuery tally.Histogram + loadedDocsPerQuery tally.Histogram + queryExhaustiveSuccess tally.Counter + queryExhaustiveInternalError tally.Counter + queryNonExhaustiveSuccess tally.Counter + queryNonExhaustiveInternalError tally.Counter + queryNonExhaustiveLimitError tally.Counter } func newNamespaceIndexMetrics( @@ -1897,6 +1924,26 @@ func newNamespaceIndexMetrics( "loaded-docs-per-query", tally.MustMakeExponentialValueBuckets(10, 2, 16), ), + queryExhaustiveSuccess: scope.Tagged(map[string]string{ + "exhaustive": "true", + "result": "success", + }).Counter("query"), + queryExhaustiveInternalError: scope.Tagged(map[string]string{ + "exhaustive": "true", + "result": "error_internal", + }).Counter("query"), + queryNonExhaustiveSuccess: scope.Tagged(map[string]string{ + "exhaustive": "false", + "result": "success", + }).Counter("query"), + queryNonExhaustiveInternalError: scope.Tagged(map[string]string{ + "exhaustive": "false", + "result": "error_internal", + }).Counter("query"), + queryNonExhaustiveLimitError: scope.Tagged(map[string]string{ + "exhaustive": "false", + "result": "error_require_exhaustive", + }).Counter("query"), } } diff --git a/src/dbnode/storage/index/types.go b/src/dbnode/storage/index/types.go index 08ebe49ed3..3e34342bc8 100644 --- a/src/dbnode/storage/index/types.go +++ b/src/dbnode/storage/index/types.go @@ -80,10 +80,11 @@ type Query struct { // QueryOptions enables users to specify constraints and // preferences on query execution. type QueryOptions struct { - StartInclusive time.Time - EndExclusive time.Time - Limit int - IterationOptions IterationOptions + StartInclusive time.Time + EndExclusive time.Time + Limit int + RequireExhaustive bool + IterationOptions IterationOptions } // IterationOptions enables users to specify iteration preferences. diff --git a/src/dbnode/storage/index_block_test.go b/src/dbnode/storage/index_block_test.go index 8e1d9b8007..f7de1b60d2 100644 --- a/src/dbnode/storage/index_block_test.go +++ b/src/dbnode/storage/index_block_test.go @@ -595,45 +595,64 @@ func TestNamespaceIndexBlockQuery(t *testing.T) { b1.EXPECT().AddResults(bootstrapResults[t1Nanos]).Return(nil) require.NoError(t, idx.Bootstrap(bootstrapResults)) - // only queries as much as is needed (wrt to time) - ctx := context.NewContext() - q := defaultQuery - qOpts := index.QueryOptions{ - StartInclusive: t0, - EndExclusive: now.Add(time.Minute), - } - - // create initial span from a mock tracer and get ctx - mtr := mocktracer.New() - sp := mtr.StartSpan("root") - ctx.SetGoContext(opentracing.ContextWithSpan(stdlibctx.Background(), sp)) - - b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - _, err = idx.Query(ctx, q, qOpts) - require.NoError(t, err) - - // queries multiple blocks if needed - qOpts = index.QueryOptions{ - StartInclusive: t0, - EndExclusive: t2.Add(time.Minute), - } - b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - b1.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - _, err = idx.Query(ctx, q, qOpts) - require.NoError(t, err) - - // stops querying once a block returns non-exhaustive - qOpts = index.QueryOptions{ - StartInclusive: t0, - EndExclusive: t0.Add(time.Minute), + for _, test := range []struct { + name string + requireExhaustive bool + }{ + {"allow non-exhaustive", false}, + {"require exhaustive", true}, + } { + t.Run(test.name, func(t *testing.T) { + // only queries as much as is needed (wrt to time) + ctx := context.NewContext() + q := defaultQuery + qOpts := index.QueryOptions{ + StartInclusive: t0, + EndExclusive: now.Add(time.Minute), + } + + // create initial span from a mock tracer and get ctx + mtr := mocktracer.New() + sp := mtr.StartSpan("root") + ctx.SetGoContext(opentracing.ContextWithSpan(stdlibctx.Background(), sp)) + + b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + result, err := idx.Query(ctx, q, qOpts) + require.NoError(t, err) + require.True(t, result.Exhaustive) + + // queries multiple blocks if needed + qOpts = index.QueryOptions{ + StartInclusive: t0, + EndExclusive: t2.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + b1.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + result, err = idx.Query(ctx, q, qOpts) + require.NoError(t, err) + require.True(t, result.Exhaustive) + + // stops querying once a block returns non-exhaustive + qOpts = index.QueryOptions{ + StartInclusive: t0, + EndExclusive: t0.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(false, nil) + result, err = idx.Query(ctx, q, qOpts) + if test.requireExhaustive { + require.Error(t, err) + } else { + require.NoError(t, err) + require.False(t, result.Exhaustive) + } + + sp.Finish() + spans := mtr.FinishedSpans() + require.Len(t, spans, 11) + }) } - b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(false, nil) - _, err = idx.Query(ctx, q, qOpts) - require.NoError(t, err) - - sp.Finish() - spans := mtr.FinishedSpans() - require.Len(t, spans, 11) } func TestNamespaceIndexBlockQueryReleasingContext(t *testing.T) { @@ -805,51 +824,71 @@ func TestNamespaceIndexBlockAggregateQuery(t *testing.T) { b1.EXPECT().AddResults(bootstrapResults[t1Nanos]).Return(nil) require.NoError(t, idx.Bootstrap(bootstrapResults)) - // only queries as much as is needed (wrt to time) - ctx := context.NewContext() - - // create initial span from a mock tracer and get ctx - mtr := mocktracer.New() - sp := mtr.StartSpan("root") - ctx.SetGoContext(opentracing.ContextWithSpan(stdlibctx.Background(), sp)) - - q := index.Query{ - Query: query, - } - qOpts := index.QueryOptions{ - StartInclusive: t0, - EndExclusive: now.Add(time.Minute), - } - aggOpts := index.AggregationOptions{QueryOptions: qOpts} - - b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - _, err = idx.AggregateQuery(ctx, q, aggOpts) - require.NoError(t, err) - - // queries multiple blocks if needed - qOpts = index.QueryOptions{ - StartInclusive: t0, - EndExclusive: t2.Add(time.Minute), - } - aggOpts = index.AggregationOptions{QueryOptions: qOpts} - b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - b1.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - _, err = idx.AggregateQuery(ctx, q, aggOpts) - require.NoError(t, err) - - // stops querying once a block returns non-exhaustive - qOpts = index.QueryOptions{ - StartInclusive: t0, - EndExclusive: t0.Add(time.Minute), + for _, test := range []struct { + name string + requireExhaustive bool + }{ + {"allow non-exhaustive", false}, + {"require exhaustive", true}, + } { + t.Run(test.name, func(t *testing.T) { + // only queries as much as is needed (wrt to time) + ctx := context.NewContext() + + // create initial span from a mock tracer and get ctx + mtr := mocktracer.New() + sp := mtr.StartSpan("root") + ctx.SetGoContext(opentracing.ContextWithSpan(stdlibctx.Background(), sp)) + + q := index.Query{ + Query: query, + } + qOpts := index.QueryOptions{ + StartInclusive: t0, + EndExclusive: now.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + aggOpts := index.AggregationOptions{QueryOptions: qOpts} + + b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + result, err := idx.AggregateQuery(ctx, q, aggOpts) + require.NoError(t, err) + require.True(t, result.Exhaustive) + + // queries multiple blocks if needed + qOpts = index.QueryOptions{ + StartInclusive: t0, + EndExclusive: t2.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + aggOpts = index.AggregationOptions{QueryOptions: qOpts} + b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + b1.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + result, err = idx.AggregateQuery(ctx, q, aggOpts) + require.NoError(t, err) + require.True(t, result.Exhaustive) + + // stops querying once a block returns non-exhaustive + qOpts = index.QueryOptions{ + StartInclusive: t0, + EndExclusive: t0.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(false, nil) + aggOpts = index.AggregationOptions{QueryOptions: qOpts} + result, err = idx.AggregateQuery(ctx, q, aggOpts) + if test.requireExhaustive { + require.Error(t, err) + } else { + require.NoError(t, err) + require.False(t, result.Exhaustive) + } + + sp.Finish() + spans := mtr.FinishedSpans() + require.Len(t, spans, 11) + }) } - b0.EXPECT().Query(gomock.Any(), gomock.Any(), q, qOpts, gomock.Any(), gomock.Any()).Return(false, nil) - aggOpts = index.AggregationOptions{QueryOptions: qOpts} - _, err = idx.AggregateQuery(ctx, q, aggOpts) - require.NoError(t, err) - - sp.Finish() - spans := mtr.FinishedSpans() - require.Len(t, spans, 11) } func TestNamespaceIndexBlockAggregateQueryReleasingContext(t *testing.T) { @@ -1036,33 +1075,52 @@ func TestNamespaceIndexBlockAggregateQueryAggPath(t *testing.T) { } aggOpts := index.AggregationOptions{QueryOptions: qOpts} - for _, query := range queries { - q := index.Query{ - Query: query, - } - b0.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - _, err = idx.AggregateQuery(ctx, q, aggOpts) - require.NoError(t, err) - - // queries multiple blocks if needed - qOpts = index.QueryOptions{ - StartInclusive: t0, - EndExclusive: t2.Add(time.Minute), - } - aggOpts = index.AggregationOptions{QueryOptions: qOpts} - b0.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - b1.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(true, nil) - _, err = idx.AggregateQuery(ctx, q, aggOpts) - require.NoError(t, err) - - // stops querying once a block returns non-exhaustive - qOpts = index.QueryOptions{ - StartInclusive: t0, - EndExclusive: t0.Add(time.Minute), - } - b0.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(false, nil) - aggOpts = index.AggregationOptions{QueryOptions: qOpts} - _, err = idx.AggregateQuery(ctx, q, aggOpts) - require.NoError(t, err) + for _, test := range []struct { + name string + requireExhaustive bool + }{ + {"allow non-exhaustive", false}, + {"require exhaustive", true}, + } { + t.Run(test.name, func(t *testing.T) { + for _, query := range queries { + q := index.Query{ + Query: query, + } + b0.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + result, err := idx.AggregateQuery(ctx, q, aggOpts) + require.NoError(t, err) + require.True(t, result.Exhaustive) + + // queries multiple blocks if needed + qOpts = index.QueryOptions{ + StartInclusive: t0, + EndExclusive: t2.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + aggOpts = index.AggregationOptions{QueryOptions: qOpts} + b0.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + b1.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(true, nil) + result, err = idx.AggregateQuery(ctx, q, aggOpts) + require.NoError(t, err) + require.True(t, result.Exhaustive) + + // stops querying once a block returns non-exhaustive + qOpts = index.QueryOptions{ + StartInclusive: t0, + EndExclusive: t0.Add(time.Minute), + RequireExhaustive: test.requireExhaustive, + } + b0.EXPECT().Aggregate(ctx, gomock.Any(), qOpts, gomock.Any(), gomock.Any()).Return(false, nil) + aggOpts = index.AggregationOptions{QueryOptions: qOpts} + result, err = idx.AggregateQuery(ctx, q, aggOpts) + if test.requireExhaustive { + require.Error(t, err) + } else { + require.NoError(t, err) + require.False(t, result.Exhaustive) + } + } + }) } } diff --git a/src/query/api/v1/handler/prometheus/handleroptions/fetch_options.go b/src/query/api/v1/handler/prometheus/handleroptions/fetch_options.go index a3d00c191f..27f26fbed0 100644 --- a/src/query/api/v1/handler/prometheus/handleroptions/fetch_options.go +++ b/src/query/api/v1/handler/prometheus/handleroptions/fetch_options.go @@ -54,7 +54,8 @@ type FetchOptionsBuilder interface { // FetchOptionsBuilderOptions provides options to use when creating a // fetch options builder. type FetchOptionsBuilderOptions struct { - Limit int + Limit int + RequireExhaustive bool } type fetchOptionsBuilder struct { diff --git a/src/query/models/query_context.go b/src/query/models/query_context.go index 6b748a4db4..f4c33eaaf9 100644 --- a/src/query/models/query_context.go +++ b/src/query/models/query_context.go @@ -44,7 +44,9 @@ type QueryContextOptions struct { // LimitMaxTimeseries limits the number of time series returned by each // storage node. LimitMaxTimeseries int - RestrictFetchType *RestrictFetchTypeQueryContextOptions + // RequireExhaustive results in an error if the query exceeds the series limit. + RequireExhaustive bool + RestrictFetchType *RestrictFetchTypeQueryContextOptions } // RestrictFetchTypeQueryContextOptions allows for specifying the diff --git a/src/query/server/query.go b/src/query/server/query.go index 2463d3f3ee..ada63a8f6b 100644 --- a/src/query/server/query.go +++ b/src/query/server/query.go @@ -276,6 +276,7 @@ func Run(runOpts RunOptions) { fetchOptsBuilder = handleroptions.NewFetchOptionsBuilder(fetchOptsBuilderCfg) queryCtxOpts = models.QueryContextOptions{ LimitMaxTimeseries: fetchOptsBuilderCfg.Limit, + RequireExhaustive: fetchOptsBuilderCfg.RequireExhaustive, } ) diff --git a/src/query/storage/index.go b/src/query/storage/index.go index 58a2790748..c74e356abf 100644 --- a/src/query/storage/index.go +++ b/src/query/storage/index.go @@ -89,9 +89,10 @@ func TagsToIdentTagIterator(tags models.Tags) ident.TagIterator { // FetchOptionsToM3Options converts a set of coordinator options to M3 options. func FetchOptionsToM3Options(fetchOptions *FetchOptions, fetchQuery *FetchQuery) index.QueryOptions { return index.QueryOptions{ - Limit: fetchOptions.Limit, - StartInclusive: fetchQuery.Start, - EndExclusive: fetchQuery.End, + Limit: fetchOptions.Limit, + RequireExhaustive: fetchOptions.RequireExhaustive, + StartInclusive: fetchQuery.Start, + EndExclusive: fetchQuery.End, } } diff --git a/src/query/storage/types.go b/src/query/storage/types.go index 3c2a64b882..acbe4e3be6 100644 --- a/src/query/storage/types.go +++ b/src/query/storage/types.go @@ -113,6 +113,8 @@ type FetchOptions struct { Remote bool // Limit is the maximum number of series to return. Limit int + // RequireExhaustive results in an error if the query exceeds the series limit. + RequireExhaustive bool // BlockType is the block type that the fetch function returns. BlockType models.FetchedBlockType // FanoutOptions are the options for the fetch namespace fanout.