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:
Peter Štibraný
2025-10-06 10:02:03 +02:00
committed by GitHub
parent cd889fef9b
commit a44af81082
7 changed files with 244 additions and 31 deletions
+49
View File
@@ -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)
}