Skip to content

Commit e41a000

Browse files
authored
Support querying multiple stores by Querier (#2747)
* Added support to querier to work with multiple stores. Store queryables now have filtering function. Signed-off-by: Peter Štibraný <[email protected]> * Querier can now use second store for querying. Signed-off-by: Peter Štibraný <[email protected]> * Fixes for using chunks and blocks at the same time. Signed-off-by: Peter Štibraný <[email protected]> * Added tests for new filtering queryables. Signed-off-by: Peter Štibraný <[email protected]> * Added test for distributor querier filter. Signed-off-by: Peter Štibraný <[email protected]> * Added test for using multiple store queryables. Signed-off-by: Peter Štibraný <[email protected]> * Ignore linter trying to get rid of else. This form allows q to have smaller scope, which reduces possibility of incorrect reuse. Signed-off-by: Peter Štibraný <[email protected]> * Allow "0" value. Signed-off-by: Peter Štibraný <[email protected]> * Mention available formats. Signed-off-by: Peter Štibraný <[email protected]> * Support for flagext.Time. Signed-off-by: Peter Štibraný <[email protected]> * Extend help for querier.use-second-store-before-time. Signed-off-by: Peter Štibraný <[email protected]> * Added CHANGELOG entry, and check to avoid using same primary and secondary engines. Signed-off-by: Peter Štibraný <[email protected]> * Put comment why formatMatcher is used. Signed-off-by: Peter Štibraný <[email protected]> * Mention that querying feature is experimental. Signed-off-by: Peter Štibraný <[email protected]> * Enhance CHANGELOG.md Signed-off-by: Peter Štibraný <[email protected]> * Explain default value. Signed-off-by: Peter Štibraný <[email protected]> * Added <time> placeholder, with example better examples. Signed-off-by: Peter Štibraný <[email protected]> * Removed `buildService` function. Signed-off-by: Peter Štibraný <[email protected]> * Mention that last two formats are specified by RFC 3339. Signed-off-by: Peter Štibraný <[email protected]> * Fix docs. Signed-off-by: Peter Štibraný <[email protected]>
1 parent f520b48 commit e41a000

File tree

14 files changed

+499
-62
lines changed

14 files changed

+499
-62
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
* `cortex_querier_blocks_consistency_checks_failed_total`
133133
* `cortex_querier_storegateway_refetches_per_query`
134134
* [ENHANCEMENT] Cortex is now built with Go 1.14. #2480 #2753
135+
* [ENHANCEMENT] Experimental: Querier can now optionally query secondary store. This is specified by using `-querier.second-store-engine` option, with values `chunks` or `tsdb`. Standard configuration options for this store are used. Additionally, this querying can be configured to happen only for queries that need data older than `-querier.use-second-store-before-time`. Default value of zero will always query secondary store. #2747
135136
* [BUGFIX] Ruler: Ensure temporary rule files with special characters are properly mapped and cleaned up. #2506
136137
* [BUGFIX] Fixes #2411, Ensure requests are properly routed to the prometheus api embedded in the query if `-server.path-prefix` is set. #2372
137138
* [BUGFIX] Experimental TSDB: fixed chunk data corruption when querying back series using the experimental blocks storage. #2400

docs/configuration/config-file-reference.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ To specify which configuration file to load, pass the `-config.file` flag at the
2222
* `<string>`: a regular string
2323
* `<url>`: an URL
2424
* `<prefix>`: a CLI flag prefix based on the context (look at the parent configuration block to see which CLI flags prefix should be used)
25+
* `<time>`: a timestamp, with available formats: `2006-01-20` (midnight, local timezone), `2006-01-20T15:04` (local timezone), and RFC 3339 formats: `2006-01-20T15:04:05Z` (UTC) or `2006-01-20T15:04:05+07:00` (explicit timezone)
2526

2627
### Use environment variables in the configuration
2728

@@ -667,6 +668,15 @@ store_gateway_client:
667668
# TLS CA path for the client
668669
# CLI flag: -experimental.querier.store-gateway-client.tls-ca-path
669670
[tls_ca_path: <string> | default = ""]
671+
672+
# Second store engine to use for querying. Empty = disabled.
673+
# CLI flag: -querier.second-store-engine
674+
[second_store_engine: <string> | default = ""]
675+
676+
# If specified, second store is only used for queries before this timestamp.
677+
# Default value 0 means secondary store is always queried.
678+
# CLI flag: -querier.use-second-store-before-time
679+
[use_second_store_before_time: <time> | default = 0]
670680
```
671681

672682
### `query_frontend_config`

docs/configuration/config-file-reference.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ To specify which configuration file to load, pass the `-config.file` flag at the
2222
* `<string>`: a regular string
2323
* `<url>`: an URL
2424
* `<prefix>`: a CLI flag prefix based on the context (look at the parent configuration block to see which CLI flags prefix should be used)
25+
* `<time>`: a timestamp, with available formats: `2006-01-20` (midnight, local timezone), `2006-01-20T15:04` (local timezone), and RFC 3339 formats: `2006-01-20T15:04:05Z` (UTC) or `2006-01-20T15:04:05+07:00` (explicit timezone)
2526

2627
### Use environment variables in the configuration
2728

docs/configuration/v1-guarantees.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ Currently experimental features are:
4545
- In-memory (FIFO) and Redis cache.
4646
- Openstack Swift storage.
4747
- gRPC Store.
48+
- Querier support for querying chunks and blocks store at the same time.

pkg/chunk/chunk_store.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ func (c *store) lookupChunksByMetricName(ctx context.Context, userID string, fro
446446
}
447447

448448
func (c *baseStore) lookupIdsByMetricNameMatcher(ctx context.Context, from, through model.Time, userID, metricName string, matcher *labels.Matcher, filter func([]IndexQuery) []IndexQuery) ([]string, error) {
449-
log, ctx := spanlogger.New(ctx, "Store.lookupIdsByMetricNameMatcher", "metricName", metricName, "matcher", matcher)
449+
log, ctx := spanlogger.New(ctx, "Store.lookupIdsByMetricNameMatcher", "metricName", metricName, "matcher", formatMatcher(matcher))
450450
defer log.Span.Finish()
451451

452452
var err error
@@ -474,11 +474,11 @@ func (c *baseStore) lookupIdsByMetricNameMatcher(ctx context.Context, from, thro
474474
if err != nil {
475475
return nil, err
476476
}
477-
level.Debug(log).Log("matcher", matcher, "queries", len(queries))
477+
level.Debug(log).Log("matcher", formatMatcher(matcher), "queries", len(queries))
478478

479479
if filter != nil {
480480
queries = filter(queries)
481-
level.Debug(log).Log("matcher", matcher, "filteredQueries", len(queries))
481+
level.Debug(log).Log("matcher", formatMatcher(matcher), "filteredQueries", len(queries))
482482
}
483483

484484
entries, err := c.lookupEntriesByQueries(ctx, queries)
@@ -489,17 +489,27 @@ func (c *baseStore) lookupIdsByMetricNameMatcher(ctx context.Context, from, thro
489489
} else if err != nil {
490490
return nil, err
491491
}
492-
level.Debug(log).Log("matcher", matcher, "entries", len(entries))
492+
level.Debug(log).Log("matcher", formatMatcher(matcher), "entries", len(entries))
493493

494494
ids, err := c.parseIndexEntries(ctx, entries, matcher)
495495
if err != nil {
496496
return nil, err
497497
}
498-
level.Debug(log).Log("matcher", matcher, "ids", len(ids))
498+
level.Debug(log).Log("matcher", formatMatcher(matcher), "ids", len(ids))
499499

500500
return ids, nil
501501
}
502502

503+
// Using this function avoids logging of nil matcher, which works, but indirectly via panic and recover.
504+
// That confuses attached debugger, which wants to breakpoint on each panic.
505+
// Using simple check is also faster.
506+
func formatMatcher(matcher *labels.Matcher) string {
507+
if matcher == nil {
508+
return "nil"
509+
}
510+
return matcher.String()
511+
}
512+
503513
func (c *baseStore) lookupEntriesByQueries(ctx context.Context, queries []IndexQuery) ([]IndexEntry, error) {
504514
log, ctx := spanlogger.New(ctx, "store.lookupEntriesByQueries")
505515
defer log.Span.Finish()

pkg/cortex/cortex.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/go-kit/kit/log/level"
1313
"github.com/opentracing/opentracing-go"
1414
"github.com/pkg/errors"
15-
prom_storage "github.com/prometheus/prometheus/storage"
1615
"github.com/thanos-io/thanos/pkg/tracing"
1716
"github.com/weaveworks/common/middleware"
1817
"github.com/weaveworks/common/server"
@@ -218,9 +217,9 @@ type Cortex struct {
218217
StoreGateway *storegateway.StoreGateway
219218
MemberlistKV *memberlist.KVInitService
220219

221-
// Queryable that the querier should use to query the long
220+
// Queryables that the querier should use to query the long
222221
// term storage. It depends on the storage engine used.
223-
StoreQueryable prom_storage.Queryable
222+
StoreQueryables []querier.QueryableWithFilter
224223
}
225224

226225
// New makes a new Cortex.

pkg/cortex/modules.go

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package cortex
33
import (
44
"fmt"
55
"os"
6+
"time"
67

78
"github.com/go-kit/kit/log/level"
89
"github.com/prometheus/client_golang/prometheus"
910
"github.com/prometheus/client_golang/prometheus/promauto"
1011
"github.com/prometheus/prometheus/promql"
12+
prom_storage "github.com/prometheus/prometheus/storage"
1113
httpgrpc_server "github.com/weaveworks/common/httpgrpc/server"
1214
"github.com/weaveworks/common/instrument"
1315
"github.com/weaveworks/common/server"
@@ -162,7 +164,7 @@ func (t *Cortex) initDistributor() (serv services.Service, err error) {
162164
}
163165

164166
func (t *Cortex) initQuerier() (serv services.Service, err error) {
165-
queryable, engine := querier.New(t.Cfg.Querier, t.Distributor, t.StoreQueryable, t.TombstonesLoader, prometheus.DefaultRegisterer)
167+
queryable, engine := querier.New(t.Cfg.Querier, t.Distributor, t.StoreQueryables, t.TombstonesLoader, prometheus.DefaultRegisterer)
166168

167169
// Prometheus histograms for requests to the querier.
168170
querierRequestDuration := promauto.With(prometheus.DefaultRegisterer).NewHistogramVec(prometheus.HistogramOpts{
@@ -194,37 +196,74 @@ func (t *Cortex) initQuerier() (serv services.Service, err error) {
194196
return worker, nil
195197
}
196198

197-
func (t *Cortex) initStoreQueryable() (services.Service, error) {
198-
if t.Cfg.Storage.Engine == storage.StorageEngineChunks {
199-
t.StoreQueryable = querier.NewChunkStoreQueryable(t.Cfg.Querier, t.Store)
200-
return nil, nil
199+
func (t *Cortex) initStoreQueryables() (services.Service, error) {
200+
var servs []services.Service
201+
202+
//nolint:golint // I prefer this form over removing 'else', because it allows q to have smaller scope.
203+
if q, err := initQueryableForEngine(t.Cfg.Storage.Engine, t.Cfg, t.Store, prometheus.DefaultRegisterer); err != nil {
204+
return nil, fmt.Errorf("failed to initialize querier for engine '%s': %v", t.Cfg.Storage.Engine, err)
205+
} else {
206+
t.StoreQueryables = append(t.StoreQueryables, querier.UseAlwaysQueryable(q))
207+
if s, ok := q.(services.Service); ok {
208+
servs = append(servs, s)
209+
}
201210
}
202211

203-
if t.Cfg.Storage.Engine == storage.StorageEngineTSDB && !t.Cfg.TSDB.StoreGatewayEnabled {
204-
storeQueryable, err := querier.NewBlockQueryable(t.Cfg.TSDB, t.Cfg.Server.LogLevel, prometheus.DefaultRegisterer)
212+
if t.Cfg.Querier.SecondStoreEngine != "" {
213+
if t.Cfg.Querier.SecondStoreEngine == t.Cfg.Storage.Engine {
214+
return nil, fmt.Errorf("second store engine used by querier '%s' must be different than primary engine '%s'", t.Cfg.Querier.SecondStoreEngine, t.Cfg.Storage.Engine)
215+
}
216+
217+
sq, err := initQueryableForEngine(t.Cfg.Querier.SecondStoreEngine, t.Cfg, t.Store, prometheus.DefaultRegisterer)
205218
if err != nil {
206-
return nil, err
219+
return nil, fmt.Errorf("failed to initialize querier for engine '%s': %v", t.Cfg.Querier.SecondStoreEngine, err)
220+
}
221+
222+
t.StoreQueryables = append(t.StoreQueryables, querier.UseBeforeTimestampQueryable(sq, time.Time(t.Cfg.Querier.UseSecondStoreBeforeTime)))
223+
224+
if s, ok := sq.(services.Service); ok {
225+
servs = append(servs, s)
207226
}
208-
t.StoreQueryable = storeQueryable
209-
return storeQueryable, nil
210227
}
211228

212-
if t.Cfg.Storage.Engine == storage.StorageEngineTSDB && t.Cfg.TSDB.StoreGatewayEnabled {
229+
// Return service, if any.
230+
switch len(servs) {
231+
case 0:
232+
return nil, nil
233+
case 1:
234+
return servs[0], nil
235+
default:
236+
// No need to support this case yet, since chunk store is not a service.
237+
// When we get there, we will need a wrapper service, that starts all subservices, and will also monitor them for failures.
238+
// Not difficult, but also not necessary right now.
239+
return nil, fmt.Errorf("too many services")
240+
}
241+
}
242+
243+
func initQueryableForEngine(engine string, cfg Config, chunkStore chunk.Store, reg prometheus.Registerer) (prom_storage.Queryable, error) {
244+
switch engine {
245+
case storage.StorageEngineChunks:
246+
if chunkStore == nil {
247+
return nil, fmt.Errorf("chunk store not initialized")
248+
}
249+
return querier.NewChunkStoreQueryable(cfg.Querier, chunkStore), nil
250+
251+
case storage.StorageEngineTSDB:
252+
if !cfg.TSDB.StoreGatewayEnabled {
253+
return querier.NewBlockQueryable(cfg.TSDB, cfg.Server.LogLevel, reg)
254+
}
255+
213256
// When running in single binary, if the blocks sharding is disabled and no custom
214257
// store-gateway address has been configured, we can set it to the running process.
215-
if t.Cfg.Target == All && !t.Cfg.StoreGateway.ShardingEnabled && t.Cfg.Querier.StoreGatewayAddresses == "" {
216-
t.Cfg.Querier.StoreGatewayAddresses = fmt.Sprintf("127.0.0.1:%d", t.Cfg.Server.GRPCListenPort)
258+
if cfg.Target == All && !cfg.StoreGateway.ShardingEnabled && cfg.Querier.StoreGatewayAddresses == "" {
259+
cfg.Querier.StoreGatewayAddresses = fmt.Sprintf("127.0.0.1:%d", cfg.Server.GRPCListenPort)
217260
}
218261

219-
storeQueryable, err := querier.NewBlocksStoreQueryableFromConfig(t.Cfg.Querier, t.Cfg.StoreGateway, t.Cfg.TSDB, util.Logger, prometheus.DefaultRegisterer)
220-
if err != nil {
221-
return nil, err
222-
}
223-
t.StoreQueryable = storeQueryable
224-
return storeQueryable, nil
225-
}
262+
return querier.NewBlocksStoreQueryableFromConfig(cfg.Querier, cfg.StoreGateway, cfg.TSDB, util.Logger, reg)
226263

227-
return nil, fmt.Errorf("unknown storage engine '%s'", t.Cfg.Storage.Engine)
264+
default:
265+
return nil, fmt.Errorf("unknown storage engine '%s'", engine)
266+
}
228267
}
229268

230269
func (t *Cortex) initIngester() (serv services.Service, err error) {
@@ -260,8 +299,8 @@ func (t *Cortex) initFlusher() (serv services.Service, err error) {
260299
return t.Flusher, nil
261300
}
262301

263-
func (t *Cortex) initStore() (serv services.Service, err error) {
264-
if t.Cfg.Storage.Engine == storage.StorageEngineTSDB {
302+
func (t *Cortex) initChunkStore() (serv services.Service, err error) {
303+
if t.Cfg.Storage.Engine != storage.StorageEngineChunks && t.Cfg.Querier.SecondStoreEngine != storage.StorageEngineChunks {
265304
return nil, nil
266305
}
267306
err = t.Cfg.Schema.Load()
@@ -411,7 +450,7 @@ func (t *Cortex) initTableManager() (services.Service, error) {
411450
func (t *Cortex) initRuler() (serv services.Service, err error) {
412451
t.Cfg.Ruler.Ring.ListenPort = t.Cfg.Server.GRPCListenPort
413452
t.Cfg.Ruler.Ring.KVStore.MemberlistKV = t.MemberlistKV.GetMemberlistKV
414-
queryable, engine := querier.New(t.Cfg.Querier, t.Distributor, t.StoreQueryable, t.TombstonesLoader, prometheus.DefaultRegisterer)
453+
queryable, engine := querier.New(t.Cfg.Querier, t.Distributor, t.StoreQueryables, t.TombstonesLoader, prometheus.DefaultRegisterer)
415454

416455
t.Ruler, err = ruler.NewRuler(t.Cfg.Ruler, engine, queryable, t.Distributor, prometheus.DefaultRegisterer, util.Logger)
417456
if err != nil {
@@ -521,12 +560,12 @@ func (t *Cortex) setupModuleManager() error {
521560
mm.RegisterModule(Ring, t.initRing)
522561
mm.RegisterModule(Overrides, t.initOverrides)
523562
mm.RegisterModule(Distributor, t.initDistributor)
524-
mm.RegisterModule(Store, t.initStore)
563+
mm.RegisterModule(Store, t.initChunkStore)
525564
mm.RegisterModule(DeleteRequestsStore, t.initDeleteRequestsStore)
526565
mm.RegisterModule(Ingester, t.initIngester)
527566
mm.RegisterModule(Flusher, t.initFlusher)
528567
mm.RegisterModule(Querier, t.initQuerier)
529-
mm.RegisterModule(StoreQueryable, t.initStoreQueryable)
568+
mm.RegisterModule(StoreQueryable, t.initStoreQueryables)
530569
mm.RegisterModule(QueryFrontend, t.initQueryFrontend)
531570
mm.RegisterModule(TableManager, t.initTableManager)
532571
mm.RegisterModule(Ruler, t.initRuler)

pkg/querier/distributor_queryable.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package querier
33
import (
44
"context"
55
"sort"
6+
"time"
67

78
"github.com/prometheus/common/model"
89
"github.com/prometheus/prometheus/pkg/labels"
@@ -14,6 +15,7 @@ import (
1415
"github.com/cortexproject/cortex/pkg/ingester/client"
1516
"github.com/cortexproject/cortex/pkg/prom1/storage/metric"
1617
"github.com/cortexproject/cortex/pkg/querier/series"
18+
"github.com/cortexproject/cortex/pkg/util"
1719
"github.com/cortexproject/cortex/pkg/util/chunkcompat"
1820
"github.com/cortexproject/cortex/pkg/util/spanlogger"
1921
)
@@ -29,17 +31,36 @@ type Distributor interface {
2931
MetricsMetadata(ctx context.Context) ([]scrape.MetricMetadata, error)
3032
}
3133

32-
func newDistributorQueryable(distributor Distributor, streaming bool, iteratorFn chunkIteratorFunc) storage.Queryable {
33-
return storage.QueryableFunc(func(ctx context.Context, mint, maxt int64) (storage.Querier, error) {
34-
return &distributorQuerier{
35-
distributor: distributor,
36-
ctx: ctx,
37-
mint: mint,
38-
maxt: maxt,
39-
streaming: streaming,
40-
chunkIterFn: iteratorFn,
41-
}, nil
42-
})
34+
func newDistributorQueryable(distributor Distributor, streaming bool, iteratorFn chunkIteratorFunc, queryIngesterWithin time.Duration) QueryableWithFilter {
35+
return distributorQueryable{
36+
distributor: distributor,
37+
streaming: streaming,
38+
iteratorFn: iteratorFn,
39+
queryIngesterWithin: queryIngesterWithin,
40+
}
41+
}
42+
43+
type distributorQueryable struct {
44+
distributor Distributor
45+
streaming bool
46+
iteratorFn chunkIteratorFunc
47+
queryIngesterWithin time.Duration
48+
}
49+
50+
func (d distributorQueryable) Querier(ctx context.Context, mint, maxt int64) (storage.Querier, error) {
51+
return &distributorQuerier{
52+
distributor: d.distributor,
53+
ctx: ctx,
54+
mint: mint,
55+
maxt: maxt,
56+
streaming: d.streaming,
57+
chunkIterFn: d.iteratorFn,
58+
}, nil
59+
}
60+
61+
func (d distributorQueryable) UseQueryable(now time.Time, _, queryMaxT int64) bool {
62+
// Include ingester only if maxt is within QueryIngestersWithin w.r.t. current time.
63+
return d.queryIngesterWithin == 0 || queryMaxT >= util.TimeToMillis(now.Add(-d.queryIngesterWithin))
4364
}
4465

4566
type distributorQuerier struct {

pkg/querier/distributor_queryable_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package querier
33
import (
44
"context"
55
"testing"
6+
"time"
67

78
"github.com/prometheus/common/model"
89
"github.com/prometheus/prometheus/pkg/labels"
@@ -15,6 +16,7 @@ import (
1516
"github.com/cortexproject/cortex/pkg/chunk/encoding"
1617
"github.com/cortexproject/cortex/pkg/ingester/client"
1718
"github.com/cortexproject/cortex/pkg/prom1/storage/metric"
19+
"github.com/cortexproject/cortex/pkg/util"
1820
"github.com/cortexproject/cortex/pkg/util/chunkcompat"
1921
)
2022

@@ -38,7 +40,7 @@ func TestDistributorQuerier(t *testing.T) {
3840
},
3941
},
4042
}
41-
queryable := newDistributorQueryable(d, false, nil)
43+
queryable := newDistributorQueryable(d, false, nil, 0)
4244
querier, err := queryable.Querier(context.Background(), mint, maxt)
4345
require.NoError(t, err)
4446

@@ -57,6 +59,22 @@ func TestDistributorQuerier(t *testing.T) {
5759
require.NoError(t, seriesSet.Err())
5860
}
5961

62+
func TestDistributorQueryableFilter(t *testing.T) {
63+
d := &mockDistributor{}
64+
dq := newDistributorQueryable(d, false, nil, 1*time.Hour)
65+
66+
now := time.Now()
67+
68+
queryMinT := util.TimeToMillis(now.Add(-5 * time.Minute))
69+
queryMaxT := util.TimeToMillis(now)
70+
71+
require.True(t, dq.UseQueryable(now, queryMinT, queryMaxT))
72+
require.True(t, dq.UseQueryable(now.Add(time.Hour), queryMinT, queryMaxT))
73+
74+
// Same query, hour+1ms later, is not sent to ingesters.
75+
require.False(t, dq.UseQueryable(now.Add(time.Hour).Add(1*time.Millisecond), queryMinT, queryMaxT))
76+
}
77+
6078
func TestIngesterStreaming(t *testing.T) {
6179
// We need to make sure that there is atleast one chunk present,
6280
// else no series will be selected.
@@ -87,7 +105,7 @@ func TestIngesterStreaming(t *testing.T) {
87105
},
88106
}
89107
ctx := user.InjectOrgID(context.Background(), "0")
90-
queryable := newDistributorQueryable(d, true, mergeChunks)
108+
queryable := newDistributorQueryable(d, true, mergeChunks, 0)
91109
querier, err := queryable.Querier(ctx, mint, maxt)
92110
require.NoError(t, err)
93111

0 commit comments

Comments
 (0)