Unified storage search: Introduce min index update interval (#111978)
* Don't update index more often than specified index_min_update_interval. * Add artificial sleep at the end of write operations. * Improve test: check for number of update calls, make diff check less flaky. * Make test less flaky by allowing for higher diff variance. * Make test less flaky by allowing for expected update calls variance.
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/dskit/backoff"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/validation"
|
||||
secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
@@ -194,6 +195,10 @@ type SearchOptions struct {
|
||||
|
||||
// Number of workers to use for index rebuilds.
|
||||
IndexRebuildWorkers int
|
||||
|
||||
// Minimum time between index updates. This is also used as a delay after a successful write operation, to guarantee
|
||||
// that subsequent search will observe the effect of the writing.
|
||||
IndexMinUpdateInterval time.Duration
|
||||
}
|
||||
|
||||
type ResourceServerOptions struct {
|
||||
@@ -336,6 +341,8 @@ func NewResourceServer(opts ResourceServerOptions) (*server, error) {
|
||||
reg: opts.Reg,
|
||||
queue: opts.QOSQueue,
|
||||
queueConfig: opts.QOSConfig,
|
||||
|
||||
artificialSuccessfulWriteDelay: opts.Search.IndexMinUpdateInterval,
|
||||
}
|
||||
|
||||
if opts.Search.Resources != nil {
|
||||
@@ -386,6 +393,11 @@ type server struct {
|
||||
reg prometheus.Registerer
|
||||
queue QOSEnqueuer
|
||||
queueConfig QueueConfig
|
||||
|
||||
// This value is used by storage server to artificially delay returning response after successful
|
||||
// write operations to make sure that subsequent search by the same client will return up-to-date results.
|
||||
// Set from SearchOptions.IndexMinUpdateInterval.
|
||||
artificialSuccessfulWriteDelay time.Duration
|
||||
}
|
||||
|
||||
// Init implements ResourceServer.
|
||||
@@ -661,6 +673,8 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re
|
||||
})
|
||||
}
|
||||
|
||||
s.sleepAfterSuccessfulWriteOperation(res, err)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -684,6 +698,37 @@ func (s *server) create(ctx context.Context, user claims.AuthInfo, req *resource
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
type responseWithErrorResult interface {
|
||||
GetError() *resourcepb.ErrorResult
|
||||
}
|
||||
|
||||
// sleepAfterSuccessfulWriteOperation will sleep for a specified time if the operation was successful.
|
||||
// Returns boolean indicating whether the sleep was performed or not (used in testing).
|
||||
//
|
||||
// This sleep is performed to guarantee search-after-write consistency, when rate-limiting updates to search index.
|
||||
func (s *server) sleepAfterSuccessfulWriteOperation(res responseWithErrorResult, err error) bool {
|
||||
if s.artificialSuccessfulWriteDelay <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// No sleep necessary if operation failed.
|
||||
return false
|
||||
}
|
||||
|
||||
// We expect that non-nil interface values with typed nils can still handle GetError() call.
|
||||
if res != nil {
|
||||
errRes := res.GetError()
|
||||
if errRes != nil {
|
||||
// No sleep necessary if operation failed.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(s.artificialSuccessfulWriteDelay)
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "storage_server.Update")
|
||||
defer span.End()
|
||||
@@ -715,6 +760,8 @@ func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*re
|
||||
})
|
||||
}
|
||||
|
||||
s.sleepAfterSuccessfulWriteOperation(res, err)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -787,6 +834,8 @@ func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*re
|
||||
})
|
||||
}
|
||||
|
||||
s.sleepAfterSuccessfulWriteOperation(res, err)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package resource
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/dskit/services"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -587,3 +589,30 @@ func newTestServerWithQueue(t *testing.T, maxSizePerTenant int, numWorkers int)
|
||||
}
|
||||
return s, q
|
||||
}
|
||||
|
||||
func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) {
|
||||
s := &server{artificialSuccessfulWriteDelay: 1 * time.Millisecond}
|
||||
|
||||
check := func(t *testing.T, expectedSleep bool, res responseWithErrorResult, err error) {
|
||||
slept := s.sleepAfterSuccessfulWriteOperation(res, err)
|
||||
require.Equal(t, expectedSleep, slept)
|
||||
}
|
||||
|
||||
// Successful responses should sleep
|
||||
check(t, true, nil, nil)
|
||||
|
||||
check(t, true, (responseWithErrorResult)((*resourcepb.CreateResponse)(nil)), nil)
|
||||
check(t, true, &resourcepb.CreateResponse{}, nil)
|
||||
|
||||
check(t, true, (responseWithErrorResult)((*resourcepb.UpdateResponse)(nil)), nil)
|
||||
check(t, true, &resourcepb.UpdateResponse{}, nil)
|
||||
|
||||
check(t, true, (responseWithErrorResult)((*resourcepb.DeleteResponse)(nil)), nil)
|
||||
check(t, true, &resourcepb.DeleteResponse{}, nil)
|
||||
|
||||
// Failed responses should return without sleeping
|
||||
check(t, false, nil, errors.New("some error"))
|
||||
check(t, false, &resourcepb.CreateResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
|
||||
check(t, false, &resourcepb.UpdateResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
|
||||
check(t, false, &resourcepb.DeleteResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user