update openapi
This commit is contained in:
@@ -59,7 +59,9 @@ type SecureValueSpec struct {
|
||||
|
||||
// The raw value is only valid for write. Read/List will always be empty.
|
||||
// There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.
|
||||
// Minimum and maximum lengths in bytes.
|
||||
// +k8s:validation:minLength=1
|
||||
// +k8s:validation:maxLength=24576
|
||||
Value ExposedSecureValue `json:"value,omitempty"`
|
||||
|
||||
// When using a third-party keeper, the `ref` is used to reference a value inside the remote storage.
|
||||
|
||||
@@ -641,8 +641,9 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
|
||||
},
|
||||
"value": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.",
|
||||
Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. Minimum and maximum lengths in bytes.",
|
||||
MinLength: ptr.To[int64](1),
|
||||
MaxLength: ptr.To[int64](24576),
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
|
||||
+18
-18
@@ -4,25 +4,25 @@ const (
|
||||
// All includes all modules necessary for Grafana to run as a standalone server
|
||||
All string = "all"
|
||||
|
||||
Core string = "core"
|
||||
MemberlistKV string = "memberlistkv"
|
||||
GrafanaAPIServer string = "grafana-apiserver"
|
||||
StorageRing string = "storage-ring"
|
||||
Distributor string = "distributor"
|
||||
StorageServer string = "storage-server"
|
||||
ZanzanaServer string = "zanzana-server"
|
||||
InstrumentationServer string = "instrumentation-server"
|
||||
FrontendServer string = "frontend-server"
|
||||
Core string = "core"
|
||||
MemberlistKV string = "memberlistkv"
|
||||
GrafanaAPIServer string = "grafana-apiserver"
|
||||
SearchServerRing string = "search-server-ring"
|
||||
SearchServerDistributor string = "search-server-distributor"
|
||||
StorageServer string = "storage-server"
|
||||
ZanzanaServer string = "zanzana-server"
|
||||
InstrumentationServer string = "instrumentation-server"
|
||||
FrontendServer string = "frontend-server"
|
||||
)
|
||||
|
||||
var dependencyMap = map[string][]string{
|
||||
MemberlistKV: {InstrumentationServer},
|
||||
StorageRing: {InstrumentationServer, MemberlistKV},
|
||||
GrafanaAPIServer: {InstrumentationServer},
|
||||
StorageServer: {InstrumentationServer, StorageRing},
|
||||
ZanzanaServer: {InstrumentationServer},
|
||||
Distributor: {InstrumentationServer, MemberlistKV, StorageRing},
|
||||
Core: {},
|
||||
All: {Core},
|
||||
FrontendServer: {},
|
||||
MemberlistKV: {InstrumentationServer},
|
||||
SearchServerRing: {InstrumentationServer, MemberlistKV},
|
||||
GrafanaAPIServer: {InstrumentationServer},
|
||||
StorageServer: {InstrumentationServer, SearchServerRing},
|
||||
ZanzanaServer: {InstrumentationServer},
|
||||
SearchServerDistributor: {InstrumentationServer, MemberlistKV, SearchServerRing},
|
||||
Core: {},
|
||||
All: {Core},
|
||||
FrontendServer: {},
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (c *Client) SendReq(ctx context.Context, url *url.URL, compatOpts CompatOpt
|
||||
return io.ReadAll(bodyReader)
|
||||
}
|
||||
|
||||
func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
|
||||
func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, expectedChecksum string, compatOpts CompatOpts) (err error) {
|
||||
// Try handling URL as a local file path first
|
||||
if _, err := os.Stat(pluginURL); err == nil {
|
||||
// TODO re-verify
|
||||
@@ -136,7 +136,7 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL,
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts)
|
||||
err = c.downloadFile(ctx, tmpFile, pluginURL, expectedChecksum, compatOpts)
|
||||
} else {
|
||||
c.retryCount = 0
|
||||
failure := fmt.Sprintf("%v", r)
|
||||
@@ -169,7 +169,7 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL,
|
||||
if c.retryCount < 3 {
|
||||
c.retryCount++
|
||||
c.log.Debug("Failed downloading. Will retry.")
|
||||
err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts)
|
||||
err = c.downloadFile(ctx, tmpFile, pluginURL, expectedChecksum, compatOpts)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -187,8 +187,9 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL,
|
||||
if err = w.Flush(); err != nil {
|
||||
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
||||
}
|
||||
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
||||
return ErrChecksumMismatch(pluginURL)
|
||||
computedChecksum := fmt.Sprintf("%x", h.Sum(nil))
|
||||
if len(expectedChecksum) > 0 && expectedChecksum != computedChecksum {
|
||||
return ErrChecksumMismatch(pluginURL, expectedChecksum, computedChecksum)
|
||||
}
|
||||
|
||||
c.retryCount = 0
|
||||
|
||||
@@ -60,7 +60,7 @@ var (
|
||||
ErrArcNotFoundBase = errutil.NotFound("plugin.archNotFound").
|
||||
MustTemplate(ErrArcNotFoundMsg, errutil.WithPublic(ErrArcNotFoundMsg))
|
||||
|
||||
ErrChecksumMismatchMsg = "expected SHA256 checksum does not match the downloaded archive ({{.Public.ArchiveURL}}) - please contact security@grafana.com"
|
||||
ErrChecksumMismatchMsg = "expected SHA256 checksum ({{.Public.ExpectedSHA256}}) does not match the downloaded archive ({{.Public.ArchiveURL}}) computed SHA256 checksum ({{.Public.ComputedSHA256}}) - please contact security@grafana.com"
|
||||
ErrChecksumMismatchBase = errutil.UnprocessableEntity("plugin.checksumMismatch").
|
||||
MustTemplate(ErrChecksumMismatchMsg, errutil.WithPublic(ErrChecksumMismatchMsg))
|
||||
|
||||
@@ -85,8 +85,8 @@ func ErrArcNotFound(pluginID, systemInfo string) error {
|
||||
return ErrArcNotFoundBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "SysInfo": systemInfo}})
|
||||
}
|
||||
|
||||
func ErrChecksumMismatch(archiveURL string) error {
|
||||
return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL}})
|
||||
func ErrChecksumMismatch(archiveURL, expectedSHA256, computedSHA256 string) error {
|
||||
return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL, "ExpectedSHA256": expectedSHA256, "ComputedSHA256": computedSHA256}})
|
||||
}
|
||||
|
||||
func ErrCorePlugin(pluginID string) error {
|
||||
|
||||
@@ -49,11 +49,13 @@ func TestErrorTemplates(t *testing.T) {
|
||||
require.Equal(t, "plugin.archNotFound", base.Public().MessageID)
|
||||
require.Equal(t, "grafana-test-app is not compatible with your system architecture: darwin-amd64", base.Public().Message)
|
||||
|
||||
err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download")
|
||||
expectedChecksum := "abcdef1234567890"
|
||||
computedChecksum := "abcdef0987654321"
|
||||
err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download", expectedChecksum, computedChecksum)
|
||||
require.True(t, errors.As(err, base))
|
||||
require.Equal(t, http.StatusUnprocessableEntity, base.Public().StatusCode)
|
||||
require.Equal(t, "plugin.checksumMismatch", base.Public().MessageID)
|
||||
require.Equal(t, "expected SHA256 checksum does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) - please contact security@grafana.com", base.Public().Message)
|
||||
require.Equal(t, "expected SHA256 checksum (abcdef1234567890) does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) computed SHA256 checksum (abcdef0987654321) - please contact security@grafana.com", base.Public().Message)
|
||||
|
||||
err = ErrCorePlugin("grafana-test-app")
|
||||
require.True(t, errors.As(err, base))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -91,11 +92,16 @@ func (r *converter) toAddCommand(ds *v0alpha1.GenericDataSource) (*datasources.A
|
||||
if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) {
|
||||
return nil, fmt.Errorf("expecting APIGroup: %s", r.group)
|
||||
}
|
||||
info, err := types.ParseNamespace(ds.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := &datasources.AddDataSourceCommand{
|
||||
Name: ds.Spec.Title,
|
||||
UID: ds.Name,
|
||||
Type: r.dstype,
|
||||
Name: ds.Spec.Title,
|
||||
UID: ds.Name,
|
||||
OrgID: info.OrgID,
|
||||
Type: r.dstype,
|
||||
|
||||
Access: datasources.DsAccess(ds.Spec.Access),
|
||||
URL: ds.Spec.URL,
|
||||
@@ -121,11 +127,16 @@ func (r *converter) toUpdateCommand(ds *v0alpha1.GenericDataSource) (*datasource
|
||||
if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) {
|
||||
return nil, fmt.Errorf("expecting APIGroup: %s", r.group)
|
||||
}
|
||||
info, err := types.ParseNamespace(ds.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := &datasources.UpdateDataSourceCommand{
|
||||
Name: ds.Spec.Title,
|
||||
UID: ds.Name,
|
||||
Type: r.dstype,
|
||||
Name: ds.Spec.Title,
|
||||
UID: ds.Name,
|
||||
OrgID: info.OrgID,
|
||||
Type: r.dstype,
|
||||
|
||||
Access: datasources.DsAccess(ds.Spec.Access),
|
||||
URL: ds.Spec.URL,
|
||||
@@ -136,15 +147,15 @@ func (r *converter) toUpdateCommand(ds *v0alpha1.GenericDataSource) (*datasource
|
||||
WithCredentials: ds.Spec.WithCredentials,
|
||||
IsDefault: ds.Spec.IsDefault,
|
||||
ReadOnly: ds.Spec.ReadOnly,
|
||||
|
||||
// The only field different than add
|
||||
Version: int(ds.Generation),
|
||||
}
|
||||
|
||||
if len(ds.Spec.JsonData.Object) > 0 {
|
||||
cmd.JsonData = simplejson.NewFromAny(ds.Spec.JsonData.Object)
|
||||
}
|
||||
cmd.SecureJsonData = toSecureJsonData(ds)
|
||||
|
||||
// The only thing differnet from the add command???
|
||||
cmd.Version = int(ds.Generation)
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
)
|
||||
|
||||
// The maximum size of a secure value in bytes when written as raw input.
|
||||
const SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES = 24576 // 24 KiB
|
||||
|
||||
type DecryptSecureValue struct {
|
||||
Keeper *string
|
||||
Ref string
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
@@ -245,6 +246,13 @@ func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admiss
|
||||
}
|
||||
|
||||
// General validations.
|
||||
if len(sv.Spec.Value) > contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES {
|
||||
errs = append(
|
||||
errs,
|
||||
field.TooLong(field.NewPath("spec", "value"), len(sv.Spec.Value), contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES),
|
||||
)
|
||||
}
|
||||
|
||||
if errs := validateDecrypters(sv.Spec.Decrypters, decryptersAllowList); len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -301,7 +309,7 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro
|
||||
return errs
|
||||
}
|
||||
|
||||
// validateDecrypters validates that (if populated) the `decrypters` must match "actor_{name}" and must be unique.
|
||||
// validateDecrypters validates that (if populated) the `decrypters` must be unique.
|
||||
func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) field.ErrorList {
|
||||
errs := make(field.ErrorList, 0)
|
||||
|
||||
@@ -319,8 +327,17 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru
|
||||
decrypterNames := make(map[string]struct{}, 0)
|
||||
|
||||
for i, decrypter := range decrypters {
|
||||
decrypter = strings.TrimSpace(decrypter)
|
||||
if decrypter == "" {
|
||||
errs = append(
|
||||
errs,
|
||||
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters cannot be empty if specified"),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Allow List: decrypters must match exactly and be in the allowed list to be able to decrypt.
|
||||
// This means an allow list item should have the format "actor_{name}" and not just "{name}".
|
||||
if len(decryptersAllowList) > 0 {
|
||||
if _, exists := decryptersAllowList[decrypter]; !exists {
|
||||
errs = append(
|
||||
@@ -334,17 +351,19 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru
|
||||
continue
|
||||
}
|
||||
|
||||
actor, name, found := strings.Cut(strings.TrimSpace(decrypter), "_")
|
||||
if !found || actor != "actor" || name == "" {
|
||||
errs = append(
|
||||
errs,
|
||||
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "a decrypter must have the format `actor_{name}`"),
|
||||
)
|
||||
// Use the same validation as labels for the decrypters.
|
||||
if verrs := validation.IsValidLabelValue(decrypter); len(verrs) > 0 {
|
||||
for _, verr := range verrs {
|
||||
errs = append(
|
||||
errs,
|
||||
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, verr),
|
||||
)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := decrypterNames[name]; exists {
|
||||
if _, exists := decrypterNames[decrypter]; exists {
|
||||
errs = append(
|
||||
errs,
|
||||
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"),
|
||||
@@ -353,7 +372,7 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru
|
||||
continue
|
||||
}
|
||||
|
||||
decrypterNames[name] = struct{}{}
|
||||
decrypterNames[decrypter] = struct{}{}
|
||||
}
|
||||
|
||||
return errs
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
func TestValidateSecureValue(t *testing.T) {
|
||||
@@ -20,7 +22,7 @@ func TestValidateSecureValue(t *testing.T) {
|
||||
Description: "description",
|
||||
Value: "value",
|
||||
Keeper: &keeper,
|
||||
Decrypters: []string{"actor_app1", "actor_app2"},
|
||||
Decrypters: []string{"app1", "app2"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,6 +52,16 @@ func TestValidateSecureValue(t *testing.T) {
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`value` cannot exceed 24576 bytes", func(t *testing.T) {
|
||||
sv := validSecureValue.DeepCopy()
|
||||
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue(strings.Repeat("a", contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES+1))
|
||||
sv.Spec.Ref = nil
|
||||
|
||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, "spec.value", errs[0].Field)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when updating a securevalue", func(t *testing.T) {
|
||||
@@ -175,8 +187,8 @@ func TestValidateSecureValue(t *testing.T) {
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{
|
||||
"actor_app1",
|
||||
"actor_app1",
|
||||
"app1",
|
||||
"app1",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -186,33 +198,8 @@ func TestValidateSecureValue(t *testing.T) {
|
||||
require.Equal(t, "spec.decrypters.[1]", errs[0].Field)
|
||||
})
|
||||
|
||||
t.Run("`decrypters` must match the expected format", func(t *testing.T) {
|
||||
ref := "ref"
|
||||
sv := &secretv0alpha1.SecureValue{
|
||||
Spec: secretv0alpha1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{
|
||||
"app1",
|
||||
"_app1",
|
||||
"actr_app1",
|
||||
"actor_ ",
|
||||
"actor_",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
|
||||
require.Len(t, errs, len(sv.Spec.Decrypters))
|
||||
|
||||
for i, err := range errs {
|
||||
require.Equal(t, fmt.Sprintf("spec.decrypters.[%d]", i), err.Field)
|
||||
require.Contains(t, err.Error(), "a decrypter must have the format `actor_{name}`")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("when set, the `decrypters` must be one of the allowed in the allow list", func(t *testing.T) {
|
||||
allowList := map[string]struct{}{"actor_app1": {}, "actor_app2": {}}
|
||||
allowList := map[string]struct{}{"app1": {}, "app2": {}}
|
||||
decrypters := slices.Collect(maps.Keys(allowList))
|
||||
|
||||
t.Run("no matches, returns an error", func(t *testing.T) {
|
||||
@@ -221,7 +208,7 @@ func TestValidateSecureValue(t *testing.T) {
|
||||
Spec: secretv0alpha1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: []string{"actor_app3"},
|
||||
Decrypters: []string{"app3"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -272,10 +259,37 @@ func TestValidateSecureValue(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("`decrypters` must be a valid label value", func(t *testing.T) {
|
||||
decrypters := []string{
|
||||
"", // invalid
|
||||
"is/this/valid", // invalid
|
||||
"is this valid", // invalid
|
||||
"is.this.valid",
|
||||
"is-this-valid",
|
||||
"is_this_valid",
|
||||
"0isthisvalid9",
|
||||
"isthisvalid9",
|
||||
"0isthisvalid",
|
||||
"isthisvalid",
|
||||
}
|
||||
|
||||
ref := "ref"
|
||||
sv := &secretv0alpha1.SecureValue{
|
||||
Spec: secretv0alpha1.SecureValueSpec{
|
||||
Description: "description", Ref: &ref,
|
||||
|
||||
Decrypters: decrypters,
|
||||
},
|
||||
}
|
||||
|
||||
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
|
||||
require.Len(t, errs, 3)
|
||||
})
|
||||
|
||||
t.Run("`decrypters` cannot have more than 64 items", func(t *testing.T) {
|
||||
decrypters := make([]string, 0, 64+1)
|
||||
for i := 0; i < 64+1; i++ {
|
||||
decrypters = append(decrypters, fmt.Sprintf("actor_app%d", i))
|
||||
decrypters = append(decrypters, fmt.Sprintf("app%d", i))
|
||||
}
|
||||
|
||||
ref := "ref"
|
||||
|
||||
@@ -53,7 +53,16 @@ func NewModule(opts Options,
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, reg prometheus.Registerer, promGatherer prometheus.Gatherer, license licensing.Licensing) (*ModuleServer, error) {
|
||||
func newModuleServer(opts Options,
|
||||
apiOpts api.ServerOptions,
|
||||
features featuremgmt.FeatureToggles,
|
||||
cfg *setting.Cfg,
|
||||
storageMetrics *resource.StorageMetrics,
|
||||
indexMetrics *resource.BleveIndexMetrics,
|
||||
reg prometheus.Registerer,
|
||||
promGatherer prometheus.Gatherer,
|
||||
license licensing.Licensing,
|
||||
) (*ModuleServer, error) {
|
||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||
|
||||
s := &ModuleServer{
|
||||
@@ -107,10 +116,10 @@ type ModuleServer struct {
|
||||
promGatherer prometheus.Gatherer
|
||||
registerer prometheus.Registerer
|
||||
|
||||
MemberlistKVConfig kv.Config
|
||||
httpServerRouter *mux.Router
|
||||
storageRing *ring.Ring
|
||||
storageRingClientPool *ringclient.Pool
|
||||
MemberlistKVConfig kv.Config
|
||||
httpServerRouter *mux.Router
|
||||
searchServerRing *ring.Ring
|
||||
searchServerRingClientPool *ringclient.Pool
|
||||
}
|
||||
|
||||
// init initializes the server and its services.
|
||||
@@ -153,8 +162,8 @@ func (s *ModuleServer) Run() error {
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.MemberlistKV, s.initMemberlistKV)
|
||||
m.RegisterModule(modules.StorageRing, s.initRing)
|
||||
m.RegisterModule(modules.Distributor, s.initDistributor)
|
||||
m.RegisterModule(modules.SearchServerRing, s.initSearchServerRing)
|
||||
m.RegisterModule(modules.SearchServerDistributor, s.initSearchServerDistributor)
|
||||
|
||||
m.RegisterModule(modules.Core, func() (services.Service, error) {
|
||||
return NewService(s.cfg, s.opts, s.apiOpts)
|
||||
@@ -174,7 +183,7 @@ func (s *ModuleServer) Run() error {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.storageRing, s.MemberlistKVConfig)
|
||||
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig)
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) {
|
||||
|
||||
+7
-7
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
var metricsPrefix = resource.RingName + "_"
|
||||
|
||||
func (ms *ModuleServer) initRing() (services.Service, error) {
|
||||
func (ms *ModuleServer) initSearchServerRing() (services.Service, error) {
|
||||
if !ms.cfg.EnableSharding {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func (ms *ModuleServer) initRing() (services.Service, error) {
|
||||
return nil, fmt.Errorf("failed to create KV store client: %s", err)
|
||||
}
|
||||
|
||||
storageRing, err := ring.NewWithStoreClientAndStrategy(
|
||||
searchServerRing, err := ring.NewWithStoreClientAndStrategy(
|
||||
toRingConfig(ms.cfg, ms.MemberlistKVConfig),
|
||||
resource.RingName,
|
||||
resource.RingKey,
|
||||
@@ -58,11 +58,11 @@ func (ms *ModuleServer) initRing() (services.Service, error) {
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize storage-ring ring: %s", err)
|
||||
return nil, fmt.Errorf("failed to initialize index-server-ring ring: %s", err)
|
||||
}
|
||||
|
||||
startFn := func(ctx context.Context) error {
|
||||
err = storageRing.StartAsync(ctx)
|
||||
err = searchServerRing.StartAsync(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start the ring: %s", err)
|
||||
}
|
||||
@@ -74,10 +74,10 @@ func (ms *ModuleServer) initRing() (services.Service, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ms.storageRing = storageRing
|
||||
ms.storageRingClientPool = pool
|
||||
ms.searchServerRing = searchServerRing
|
||||
ms.searchServerRingClientPool = pool
|
||||
|
||||
ms.httpServerRouter.Path("/ring").Methods("GET", "POST").Handler(storageRing)
|
||||
ms.httpServerRouter.Path("/ring").Methods("GET", "POST").Handler(searchServerRing)
|
||||
|
||||
svc := services.NewIdleService(startFn, nil)
|
||||
|
||||
|
||||
@@ -10,18 +10,18 @@ import (
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
func (ms *ModuleServer) initDistributor() (services.Service, error) {
|
||||
func (ms *ModuleServer) initSearchServerDistributor() (services.Service, error) {
|
||||
var (
|
||||
distributor = &distributorService{}
|
||||
tracer = otel.Tracer("unified-storage-distributor")
|
||||
tracer = otel.Tracer("index-server-distributor")
|
||||
err error
|
||||
)
|
||||
distributor.grpcHandler, err = resource.ProvideDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.storageRing, ms.storageRingClientPool)
|
||||
distributor.grpcHandler, err = resource.ProvideSearchDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.searchServerRing, ms.searchServerRingClientPool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return services.NewBasicService(nil, distributor.running, nil).WithName(modules.Distributor), nil
|
||||
return services.NewBasicService(nil, distributor.running, nil).WithName(modules.SearchServerDistributor), nil
|
||||
}
|
||||
|
||||
type distributorService struct {
|
||||
@@ -273,7 +273,7 @@ func initDistributorServerForTest(t *testing.T, memberlistPort int) testModuleSe
|
||||
cfg.MemberlistJoinMember = "127.0.0.1:" + strconv.Itoa(memberlistPort)
|
||||
cfg.MemberlistAdvertiseAddr = "127.0.0.1"
|
||||
cfg.MemberlistAdvertisePort = memberlistPort
|
||||
cfg.Target = []string{modules.Distributor}
|
||||
cfg.Target = []string{modules.SearchServerDistributor}
|
||||
cfg.InstanceID = "distributor" // does nothing for the distributor but may be useful to debug tests
|
||||
|
||||
conn, err := grpc.NewClient(cfg.GRPCServer.Address,
|
||||
@@ -352,7 +352,18 @@ func createBaselineServer(t *testing.T, dbType, dbConnStr string, testNamespaces
|
||||
require.NoError(t, err)
|
||||
searchOpts, err := search.NewSearchOptions(features, cfg, tracer, docBuilders, nil)
|
||||
require.NoError(t, err)
|
||||
server, err := sql.NewResourceServer(nil, cfg, tracer, nil, nil, searchOpts, nil, nil, features)
|
||||
server, err := sql.NewResourceServer(sql.ServerOptions{
|
||||
DB: nil,
|
||||
Cfg: cfg,
|
||||
Tracer: tracer,
|
||||
Reg: nil,
|
||||
AccessClient: nil,
|
||||
SearchOptions: searchOpts,
|
||||
StorageMetrics: nil,
|
||||
IndexMetrics: nil,
|
||||
Features: features,
|
||||
QOSQueue: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
testUserA := &identity.StaticRequester{
|
||||
File diff suppressed because one or more lines are too long
@@ -697,13 +697,6 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
Name: "regressionTransformation",
|
||||
Description: "Enables regression analysis transformation",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
// this is mainly used as a way to quickly disable query hints as a safeguard for our infrastructure
|
||||
Name: "lokiQueryHints",
|
||||
@@ -1145,7 +1138,8 @@ var (
|
||||
{
|
||||
Name: "improvedExternalSessionHandling",
|
||||
Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Expression: "true", // enabled by default
|
||||
Owner: identityAccessTeam,
|
||||
AllowSelfServe: true,
|
||||
},
|
||||
@@ -1367,7 +1361,8 @@ var (
|
||||
{
|
||||
Name: "improvedExternalSessionHandlingSAML",
|
||||
Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Expression: "true", // enabled by default
|
||||
Owner: identityAccessTeam,
|
||||
AllowSelfServe: true,
|
||||
},
|
||||
@@ -1675,15 +1670,6 @@ var (
|
||||
HideFromDocs: true,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
{
|
||||
Name: "extensionsReadOnlyProxy",
|
||||
Description: "Use proxy-based read-only objects for plugin extensions instead of deep cloning",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesAuthzApis",
|
||||
Description: "Registers AuthZ /apis endpoint",
|
||||
|
||||
@@ -92,7 +92,6 @@ logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true
|
||||
logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true
|
||||
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true
|
||||
regressionTransformation,preview,@grafana/dataviz-squad,false,false,true
|
||||
lokiQueryHints,GA,@grafana/observability-logs,false,false,true
|
||||
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
|
||||
@@ -148,7 +147,7 @@ exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false
|
||||
appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false
|
||||
groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false
|
||||
alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true
|
||||
improvedExternalSessionHandling,preview,@grafana/identity-access-team,false,false,false
|
||||
improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false
|
||||
useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false
|
||||
rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
|
||||
unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false
|
||||
@@ -179,7 +178,7 @@ lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false
|
||||
investigationsBackend,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
|
||||
k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false
|
||||
improvedExternalSessionHandlingSAML,preview,@grafana/identity-access-team,false,false,false
|
||||
improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false
|
||||
teamHttpHeadersMimir,GA,@grafana/identity-access-team,false,false,false
|
||||
teamHttpHeadersTempo,experimental,@grafana/identity-access-team,false,false,false
|
||||
templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,false,false,true
|
||||
@@ -219,7 +218,6 @@ multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false,
|
||||
alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true
|
||||
alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
|
||||
extensionsReadOnlyProxy,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false
|
||||
restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
skipTokenRotationIfRecent,privatePreview,@grafana/identity-access-team,false,false,false
|
||||
|
||||
|
@@ -379,10 +379,6 @@ const (
|
||||
// Enables shared crosshair in table panel
|
||||
FlagTableSharedCrosshair = "tableSharedCrosshair"
|
||||
|
||||
// FlagRegressionTransformation
|
||||
// Enables regression analysis transformation
|
||||
FlagRegressionTransformation = "regressionTransformation"
|
||||
|
||||
// FlagLokiQueryHints
|
||||
// Enables query hints for Loki
|
||||
FlagLokiQueryHints = "lokiQueryHints"
|
||||
@@ -887,10 +883,6 @@ const (
|
||||
// Enables the alerting bulk actions in the UI
|
||||
FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI"
|
||||
|
||||
// FlagExtensionsReadOnlyProxy
|
||||
// Use proxy-based read-only objects for plugin extensions instead of deep cloning
|
||||
FlagExtensionsReadOnlyProxy = "extensionsReadOnlyProxy"
|
||||
|
||||
// FlagKubernetesAuthzApis
|
||||
// Registers AuthZ /apis endpoint
|
||||
FlagKubernetesAuthzApis = "kubernetesAuthzApis"
|
||||
|
||||
@@ -1110,7 +1110,8 @@
|
||||
"metadata": {
|
||||
"name": "extensionsReadOnlyProxy",
|
||||
"resourceVersion": "1750434297879",
|
||||
"creationTimestamp": "2025-05-06T04:55:23Z"
|
||||
"creationTimestamp": "2025-05-06T04:55:23Z",
|
||||
"deletionTimestamp": "2025-06-30T08:24:11Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use proxy-based read-only objects for plugin extensions instead of deep cloning",
|
||||
@@ -1398,27 +1399,35 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "improvedExternalSessionHandling",
|
||||
"resourceVersion": "1750434297879",
|
||||
"creationTimestamp": "2024-09-17T10:54:39Z"
|
||||
"resourceVersion": "1751355094344",
|
||||
"creationTimestamp": "2024-09-17T10:54:39Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.",
|
||||
"stage": "preview",
|
||||
"stage": "GA",
|
||||
"codeowner": "@grafana/identity-access-team",
|
||||
"allowSelfServe": true
|
||||
"allowSelfServe": true,
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "improvedExternalSessionHandlingSAML",
|
||||
"resourceVersion": "1750434297879",
|
||||
"creationTimestamp": "2025-01-09T17:02:49Z"
|
||||
"resourceVersion": "1751355094344",
|
||||
"creationTimestamp": "2025-01-09T17:02:49Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.",
|
||||
"stage": "preview",
|
||||
"stage": "GA",
|
||||
"codeowner": "@grafana/identity-access-team",
|
||||
"allowSelfServe": true
|
||||
"allowSelfServe": true,
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2606,7 +2615,8 @@
|
||||
"metadata": {
|
||||
"name": "regressionTransformation",
|
||||
"resourceVersion": "1750434297879",
|
||||
"creationTimestamp": "2023-11-24T14:49:16Z"
|
||||
"creationTimestamp": "2023-11-24T14:49:16Z",
|
||||
"deletionTimestamp": "2025-07-01T13:24:02Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables regression analysis transformation",
|
||||
|
||||
@@ -567,6 +567,9 @@ type Cfg struct {
|
||||
IndexRebuildInterval time.Duration
|
||||
IndexCacheTTL time.Duration
|
||||
EnableSharding bool
|
||||
QOSEnabled bool
|
||||
QOSNumberWorker int
|
||||
QOSMaxSizePerTenant int
|
||||
MemberlistBindAddr string
|
||||
MemberlistAdvertiseAddr string
|
||||
MemberlistAdvertisePort int
|
||||
|
||||
@@ -49,13 +49,16 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
|
||||
}
|
||||
cfg.UnifiedStorage = storageConfig
|
||||
|
||||
// Set indexer config for unified storaae
|
||||
// Set indexer config for unified storage
|
||||
section := cfg.Raw.Section("unified_storage")
|
||||
cfg.MaxPageSizeBytes = section.Key("max_page_size_bytes").MustInt(0)
|
||||
cfg.IndexPath = section.Key("index_path").String()
|
||||
cfg.IndexWorkers = section.Key("index_workers").MustInt(10)
|
||||
cfg.IndexMaxBatchSize = section.Key("index_max_batch_size").MustInt(100)
|
||||
cfg.EnableSharding = section.Key("enable_sharding").MustBool(false)
|
||||
cfg.QOSEnabled = section.Key("qos_enabled").MustBool(false)
|
||||
cfg.QOSNumberWorker = section.Key("qos_num_worker").MustInt(16)
|
||||
cfg.QOSMaxSizePerTenant = section.Key("qos_max_size_per_tenant").MustInt(1000)
|
||||
cfg.MemberlistBindAddr = section.Key("memberlist_bind_addr").String()
|
||||
cfg.MemberlistAdvertiseAddr = section.Key("memberlist_advertise_addr").String()
|
||||
cfg.MemberlistAdvertisePort = section.Key("memberlist_advertise_port").MustInt(7946)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/dskit/flagext"
|
||||
"github.com/grafana/dskit/grpcclient"
|
||||
"github.com/grafana/dskit/middleware"
|
||||
"github.com/grafana/dskit/services"
|
||||
|
||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
||||
"github.com/grafana/grafana/pkg/util/scheduler"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -49,7 +51,10 @@ type clientMetrics struct {
|
||||
}
|
||||
|
||||
// This adds a UnifiedStorage client into the wire dependency tree
|
||||
func ProvideUnifiedStorageClient(opts *Options, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics) (resource.ResourceClient, error) {
|
||||
func ProvideUnifiedStorageClient(opts *Options,
|
||||
storageMetrics *resource.StorageMetrics,
|
||||
indexMetrics *resource.BleveIndexMetrics,
|
||||
) (resource.ResourceClient, error) {
|
||||
// See: apiserver.applyAPIServerConfig(cfg, features, o)
|
||||
apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver")
|
||||
client, err := newClient(options.StorageOptions{
|
||||
@@ -83,6 +88,7 @@ func newClient(opts options.StorageOptions,
|
||||
indexMetrics *resource.BleveIndexMetrics,
|
||||
) (resource.ResourceClient, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
switch opts.StorageType {
|
||||
case options.StorageTypeFile:
|
||||
if opts.DataPath == "" {
|
||||
@@ -146,13 +152,50 @@ func newClient(opts options.StorageOptions,
|
||||
}
|
||||
return client, nil
|
||||
|
||||
// Use the local SQL
|
||||
default:
|
||||
searchOptions, err := search.NewSearchOptions(features, cfg, tracer, docs, indexMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server, err := sql.NewResourceServer(db, cfg, tracer, reg, authzc, searchOptions, storageMetrics, indexMetrics, features)
|
||||
|
||||
serverOptions := sql.ServerOptions{
|
||||
DB: db,
|
||||
Cfg: cfg,
|
||||
Tracer: tracer,
|
||||
Reg: reg,
|
||||
AccessClient: authzc,
|
||||
SearchOptions: searchOptions,
|
||||
StorageMetrics: storageMetrics,
|
||||
IndexMetrics: indexMetrics,
|
||||
Features: features,
|
||||
}
|
||||
|
||||
if cfg.QOSEnabled {
|
||||
qosReg := prometheus.WrapRegistererWithPrefix("resource_server_qos_", reg)
|
||||
queue := scheduler.NewQueue(&scheduler.QueueOptions{
|
||||
MaxSizePerTenant: cfg.QOSMaxSizePerTenant,
|
||||
Registerer: qosReg,
|
||||
Logger: cfg.Logger,
|
||||
})
|
||||
if err := services.StartAndAwaitRunning(ctx, queue); err != nil {
|
||||
return nil, fmt.Errorf("failed to start queue: %w", err)
|
||||
}
|
||||
scheduler, err := scheduler.NewScheduler(queue, &scheduler.Config{
|
||||
NumWorkers: cfg.QOSNumberWorker,
|
||||
Logger: cfg.Logger,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create scheduler: %w", err)
|
||||
}
|
||||
|
||||
err = services.StartAndAwaitRunning(ctx, scheduler)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start scheduler: %w", err)
|
||||
}
|
||||
serverOptions.QOSQueue = queue
|
||||
}
|
||||
|
||||
server, err := sql.NewResourceServer(serverOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/util/scheduler"
|
||||
)
|
||||
|
||||
// Package-level errors.
|
||||
@@ -50,6 +51,14 @@ func NewNotFoundError(key *resourcepb.ResourceKey) *resourcepb.ErrorResult {
|
||||
}
|
||||
}
|
||||
|
||||
func NewTooManyRequestsError(msg string) *resourcepb.ErrorResult {
|
||||
return &resourcepb.ErrorResult{
|
||||
Message: msg,
|
||||
Code: http.StatusTooManyRequests,
|
||||
Reason: string(metav1.StatusReasonTooManyRequests),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert golang errors to status result errors that can be returned to a client
|
||||
func AsErrorResult(err error) *resourcepb.ErrorResult {
|
||||
if err == nil {
|
||||
@@ -125,3 +134,10 @@ func GetError(res *resourcepb.ErrorResult) error {
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func HandleQueueError[T any](err error, makeResp func(*resourcepb.ErrorResult) *T) (*T, error) {
|
||||
if errors.Is(err, scheduler.ErrTenantQueueFull) {
|
||||
return makeResp(NewTooManyRequestsError("tenant queue is full, please try again later")), nil
|
||||
}
|
||||
return makeResp(AsErrorResult(err)), nil
|
||||
}
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@ import (
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func ProvideDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer trace.Tracer, ring *ring.Ring, ringClientPool *ringclient.Pool) (grpcserver.Provider, error) {
|
||||
func ProvideSearchDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer trace.Tracer, ring *ring.Ring, ringClientPool *ringclient.Pool) (grpcserver.Provider, error) {
|
||||
var err error
|
||||
grpcHandler, err := grpcserver.ProvideService(cfg, features, nil, tracer, registerer)
|
||||
if err != nil {
|
||||
@@ -29,7 +29,7 @@ func ProvideDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureTogg
|
||||
}
|
||||
|
||||
distributorServer := &distributorServer{
|
||||
log: log.New("unified-storage-distributor"),
|
||||
log: log.New("index-server-distributor"),
|
||||
ring: ring,
|
||||
clientPool: ringClientPool,
|
||||
}
|
||||
@@ -73,8 +73,8 @@ func (c *RingClient) RemoteAddress() string {
|
||||
return c.Conn.Target()
|
||||
}
|
||||
|
||||
const RingKey = "unified-storage-ring"
|
||||
const RingName = "unified_storage_ring"
|
||||
const RingKey = "search-server-ring"
|
||||
const RingName = "search_server_ring"
|
||||
const RingHeartbeatTimeout = time.Minute
|
||||
const RingNumTokens = 128
|
||||
|
||||
@@ -19,9 +19,20 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/dskit/backoff"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/util/scheduler"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultMaxBackoff is the default maximum backoff duration for enqueue operations.
|
||||
DefaultMaxBackoff = 1 * time.Second
|
||||
// DefaultMinBackoff is the default minimum backoff duration for enqueue operations.
|
||||
DefaultMinBackoff = 100 * time.Millisecond
|
||||
// DefaultMaxRetries is the default maximum number of retries for enqueue operations.
|
||||
DefaultMaxRetries = 3
|
||||
)
|
||||
|
||||
// ResourceServer implements all gRPC services
|
||||
@@ -134,6 +145,10 @@ type BlobSupport interface {
|
||||
// TODO? List+Delete? This is for admin access
|
||||
}
|
||||
|
||||
type QOSEnqueuer interface {
|
||||
Enqueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error
|
||||
}
|
||||
|
||||
type BlobConfig struct {
|
||||
// The CDK configuration URL
|
||||
URL string
|
||||
@@ -203,7 +218,11 @@ type ResourceServerOptions struct {
|
||||
|
||||
IndexMetrics *BleveIndexMetrics
|
||||
|
||||
// MaxPageSizeBytes is the maximum size of a page in bytes.
|
||||
MaxPageSizeBytes int
|
||||
|
||||
// QOSQueue is the quality of service queue used to enqueue
|
||||
QOSQueue QOSEnqueuer
|
||||
}
|
||||
|
||||
func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
|
||||
@@ -222,6 +241,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
|
||||
if opts.Diagnostics == nil {
|
||||
opts.Diagnostics = &noopService{}
|
||||
}
|
||||
|
||||
if opts.Now == nil {
|
||||
opts.Now = func() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
@@ -233,6 +253,10 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
|
||||
opts.MaxPageSizeBytes = 1024 * 1024 * 2
|
||||
}
|
||||
|
||||
if opts.QOSQueue == nil {
|
||||
opts.QOSQueue = scheduler.NewNoopQueue()
|
||||
}
|
||||
|
||||
// Initialize the blob storage
|
||||
blobstore := opts.Blob.Backend
|
||||
if blobstore == nil {
|
||||
@@ -275,6 +299,8 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
|
||||
storageMetrics: opts.storageMetrics,
|
||||
indexMetrics: opts.IndexMetrics,
|
||||
maxPageSizeBytes: opts.MaxPageSizeBytes,
|
||||
reg: opts.Reg,
|
||||
queue: opts.QOSQueue,
|
||||
}
|
||||
|
||||
if opts.Search.Resources != nil {
|
||||
@@ -321,6 +347,8 @@ type server struct {
|
||||
initErr error
|
||||
|
||||
maxPageSizeBytes int
|
||||
reg prometheus.Registerer
|
||||
queue QOSEnqueuer
|
||||
}
|
||||
|
||||
// Init implements ResourceServer.
|
||||
@@ -570,6 +598,25 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
var (
|
||||
res *resourcepb.CreateResponse
|
||||
err error
|
||||
)
|
||||
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
|
||||
res, err = s.create(ctx, user, req)
|
||||
})
|
||||
if runErr != nil {
|
||||
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.CreateResponse {
|
||||
return &resourcepb.CreateResponse{Error: e}
|
||||
})
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *server) create(ctx context.Context, user claims.AuthInfo, req *resourcepb.CreateRequest) (*resourcepb.CreateResponse, error) {
|
||||
rsp := &resourcepb.CreateResponse{}
|
||||
|
||||
event, e := s.newEvent(ctx, user, req.Key, req.Value, nil)
|
||||
if e != nil {
|
||||
rsp.Error = e
|
||||
@@ -605,6 +652,24 @@ func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*re
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
var (
|
||||
res *resourcepb.UpdateResponse
|
||||
err error
|
||||
)
|
||||
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
|
||||
res, err = s.update(ctx, user, req)
|
||||
})
|
||||
if runErr != nil {
|
||||
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.UpdateResponse {
|
||||
return &resourcepb.UpdateResponse{Error: e}
|
||||
})
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *server) update(ctx context.Context, user claims.AuthInfo, req *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) {
|
||||
rsp := &resourcepb.UpdateResponse{}
|
||||
latest := s.backend.ReadResource(ctx, &resourcepb.ReadRequest{
|
||||
Key: req.Key,
|
||||
})
|
||||
@@ -654,6 +719,25 @@ func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*re
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
var (
|
||||
res *resourcepb.DeleteResponse
|
||||
err error
|
||||
)
|
||||
|
||||
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
|
||||
res, err = s.delete(ctx, user, req)
|
||||
})
|
||||
if runErr != nil {
|
||||
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.DeleteResponse {
|
||||
return &resourcepb.DeleteResponse{Error: e}
|
||||
})
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resourcepb.DeleteRequest) (*resourcepb.DeleteResponse, error) {
|
||||
rsp := &resourcepb.DeleteResponse{}
|
||||
latest := s.backend.ReadResource(ctx, &resourcepb.ReadRequest{
|
||||
Key: req.Key,
|
||||
})
|
||||
@@ -744,6 +828,23 @@ func (s *server) Read(ctx context.Context, req *resourcepb.ReadRequest) (*resour
|
||||
return &resourcepb.ReadResponse{Error: NewBadRequestError("missing resource")}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
res *resourcepb.ReadResponse
|
||||
err error
|
||||
)
|
||||
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
|
||||
res, err = s.read(ctx, user, req)
|
||||
})
|
||||
if runErr != nil {
|
||||
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.ReadResponse {
|
||||
return &resourcepb.ReadResponse{Error: e}
|
||||
})
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *server) read(ctx context.Context, user claims.AuthInfo, req *resourcepb.ReadRequest) (*resourcepb.ReadResponse, error) {
|
||||
rsp := s.backend.ReadResource(ctx, req)
|
||||
if rsp.Error != nil && rsp.Error.Code == http.StatusNotFound {
|
||||
return &resourcepb.ReadResponse{Error: rsp.Error}, nil
|
||||
@@ -1237,3 +1338,41 @@ func (s *server) GetBlob(ctx context.Context, req *resourcepb.GetBlobRequest) (*
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (s *server) runInQueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error {
|
||||
boff := backoff.New(ctx, backoff.Config{
|
||||
MinBackoff: DefaultMinBackoff,
|
||||
MaxBackoff: DefaultMaxBackoff,
|
||||
MaxRetries: DefaultMaxRetries,
|
||||
})
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
err error
|
||||
)
|
||||
wg.Add(1)
|
||||
wrapped := func(ctx context.Context) {
|
||||
runnable(ctx)
|
||||
wg.Done()
|
||||
}
|
||||
for boff.Ongoing() {
|
||||
err = s.queue.Enqueue(ctx, tenantID, wrapped)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
s.log.Warn("failed to enqueue runnable, retrying",
|
||||
"maxRetries", DefaultMaxRetries,
|
||||
"tenantID", tenantID,
|
||||
"error", err)
|
||||
boff.Wait()
|
||||
}
|
||||
if err != nil {
|
||||
s.log.Error("failed to enqueue runnable",
|
||||
"maxRetries", DefaultMaxRetries,
|
||||
"tenantID", tenantID,
|
||||
"error", err)
|
||||
return fmt.Errorf("failed to enqueue runnable for tenant %s: %w", tenantID, err)
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,9 +87,10 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
engine.SetMaxOpenConns(0)
|
||||
engine.SetMaxIdleConns(2)
|
||||
engine.SetConnMaxLifetime(4 * time.Hour)
|
||||
engine.SetMaxOpenConns(getter.Int("max_open_conn", 0))
|
||||
engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4))
|
||||
maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second
|
||||
engine.SetConnMaxLifetime(maxLifetime)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
@@ -188,5 +189,10 @@ func getEnginePostgres(getter confGetter) (*xorm.Engine, error) {
|
||||
return nil, fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
|
||||
engine.SetMaxOpenConns(getter.Int("max_open_conn", 0))
|
||||
engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4))
|
||||
maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second
|
||||
engine.SetConnMaxLifetime(maxLifetime)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type confGetter interface {
|
||||
Err() error
|
||||
Bool(key string) bool
|
||||
String(key string) string
|
||||
Int(key string, def int) int
|
||||
}
|
||||
|
||||
func newConfGetter(ds *setting.DynamicSection, keyPrefix string) confGetter {
|
||||
@@ -52,6 +53,10 @@ func (g *sectionGetter) String(key string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
func (g *sectionGetter) Int(key string, def int) int {
|
||||
return g.ds.Key(g.keyPrefix + key).MustInt(def)
|
||||
}
|
||||
|
||||
// MakeDSN creates a DSN from the given key/value pair. It validates the strings
|
||||
// form valid UTF-8 sequences and escapes values if needed.
|
||||
func MakeDSN(m map[string]string) (string, error) {
|
||||
|
||||
@@ -28,11 +28,13 @@ func TestSectionGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
key = "the key"
|
||||
keyBoolTrue = "I'm true"
|
||||
keyBoolFalse = "not me!"
|
||||
prefix = "this is some prefix"
|
||||
val = string(invalidUTF8ByteSequence)
|
||||
key = "the key"
|
||||
keyBoolTrue = "I'm true"
|
||||
keyBoolFalse = "not me!"
|
||||
keyIntValid = "valid_int"
|
||||
keyIntMissing = "missing_int"
|
||||
prefix = "this is some prefix"
|
||||
val = string(invalidUTF8ByteSequence)
|
||||
)
|
||||
|
||||
t.Run("with prefix", func(t *testing.T) {
|
||||
@@ -42,6 +44,8 @@ func TestSectionGetter(t *testing.T) {
|
||||
prefix + key: val,
|
||||
prefix + keyBoolTrue: "YES",
|
||||
prefix + keyBoolFalse: "0",
|
||||
prefix + keyIntValid: "42",
|
||||
// Note: keyIntMissing is intentionally not included to test default behavior
|
||||
}, prefix)
|
||||
|
||||
require.False(t, g.Bool("whatever bool"))
|
||||
@@ -53,6 +57,15 @@ func TestSectionGetter(t *testing.T) {
|
||||
require.True(t, g.Bool(keyBoolTrue))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Equal(t, 999, g.Int("whatever int", 999))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Equal(t, 42, g.Int(keyIntValid, 100))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Equal(t, 200, g.Int(keyIntMissing, 200))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Empty(t, g.String("whatever string"))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
@@ -68,6 +81,8 @@ func TestSectionGetter(t *testing.T) {
|
||||
key: val,
|
||||
keyBoolTrue: "true",
|
||||
keyBoolFalse: "f",
|
||||
keyIntValid: "123",
|
||||
// Note: keyIntMissing is intentionally not included to test default behavior
|
||||
}, "")
|
||||
|
||||
require.False(t, g.Bool("whatever bool"))
|
||||
@@ -79,6 +94,15 @@ func TestSectionGetter(t *testing.T) {
|
||||
require.True(t, g.Bool(keyBoolTrue))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Equal(t, 500, g.Int("whatever int", 500))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Equal(t, 123, g.Int(keyIntValid, 0))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Equal(t, 300, g.Int(keyIntMissing, 300))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
require.Empty(t, g.String("whatever string"))
|
||||
require.NoError(t, g.Err())
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/dskit/services"
|
||||
|
||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -17,70 +19,85 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
|
||||
)
|
||||
|
||||
type QOSEnqueueDequeuer interface {
|
||||
services.Service
|
||||
Enqueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error
|
||||
Dequeue(ctx context.Context) (func(ctx context.Context), error)
|
||||
}
|
||||
|
||||
// ServerOptions contains the options for creating a new ResourceServer
|
||||
type ServerOptions struct {
|
||||
DB infraDB.DB
|
||||
Cfg *setting.Cfg
|
||||
Tracer trace.Tracer
|
||||
Reg prometheus.Registerer
|
||||
AccessClient types.AccessClient
|
||||
SearchOptions resource.SearchOptions
|
||||
StorageMetrics *resource.StorageMetrics
|
||||
IndexMetrics *resource.BleveIndexMetrics
|
||||
Features featuremgmt.FeatureToggles
|
||||
QOSQueue QOSEnqueueDequeuer
|
||||
}
|
||||
|
||||
// Creates a new ResourceServer
|
||||
func NewResourceServer(db infraDB.DB, cfg *setting.Cfg,
|
||||
tracer trace.Tracer, reg prometheus.Registerer, ac types.AccessClient,
|
||||
searchOptions resource.SearchOptions, storageMetrics *resource.StorageMetrics,
|
||||
indexMetrics *resource.BleveIndexMetrics, features featuremgmt.FeatureToggles) (resource.ResourceServer, error) {
|
||||
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
|
||||
opts := resource.ResourceServerOptions{
|
||||
Tracer: tracer,
|
||||
func NewResourceServer(
|
||||
opts ServerOptions,
|
||||
) (resource.ResourceServer, error) {
|
||||
apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver")
|
||||
serverOptions := resource.ResourceServerOptions{
|
||||
Tracer: opts.Tracer,
|
||||
Blob: resource.BlobConfig{
|
||||
URL: apiserverCfg.Key("blob_url").MustString(""),
|
||||
},
|
||||
Reg: reg,
|
||||
Reg: opts.Reg,
|
||||
}
|
||||
if ac != nil {
|
||||
opts.AccessClient = resource.NewAuthzLimitedClient(ac, resource.AuthzOptions{Tracer: tracer, Registry: reg})
|
||||
if opts.AccessClient != nil {
|
||||
serverOptions.AccessClient = resource.NewAuthzLimitedClient(opts.AccessClient, resource.AuthzOptions{Tracer: opts.Tracer, Registry: opts.Reg})
|
||||
}
|
||||
// Support local file blob
|
||||
if strings.HasPrefix(opts.Blob.URL, "./data/") {
|
||||
dir := strings.Replace(opts.Blob.URL, "./data", cfg.DataPath, 1)
|
||||
if strings.HasPrefix(serverOptions.Blob.URL, "./data/") {
|
||||
dir := strings.Replace(serverOptions.Blob.URL, "./data", opts.Cfg.DataPath, 1)
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.Blob.URL = "file:///" + dir
|
||||
serverOptions.Blob.URL = "file:///" + dir
|
||||
}
|
||||
|
||||
// This is mostly for testing, being able to influence when we paginate
|
||||
// based on the page size during tests.
|
||||
unifiedStorageCfg := cfg.SectionWithEnvOverrides("unified_storage")
|
||||
unifiedStorageCfg := opts.Cfg.SectionWithEnvOverrides("unified_storage")
|
||||
maxPageSizeBytes := unifiedStorageCfg.Key("max_page_size_bytes")
|
||||
opts.MaxPageSizeBytes = maxPageSizeBytes.MustInt(0)
|
||||
serverOptions.MaxPageSizeBytes = maxPageSizeBytes.MustInt(0)
|
||||
|
||||
eDB, err := dbimpl.ProvideResourceDB(db, cfg, tracer)
|
||||
eDB, err := dbimpl.ProvideResourceDB(opts.DB, opts.Cfg, opts.Tracer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isHA := isHighAvailabilityEnabled(cfg.SectionWithEnvOverrides("database"),
|
||||
cfg.SectionWithEnvOverrides("resource_api"))
|
||||
withPruner := features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageHistoryPruner)
|
||||
isHA := isHighAvailabilityEnabled(opts.Cfg.SectionWithEnvOverrides("database"),
|
||||
opts.Cfg.SectionWithEnvOverrides("resource_api"))
|
||||
withPruner := opts.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageHistoryPruner)
|
||||
|
||||
store, err := NewBackend(BackendOptions{
|
||||
DBProvider: eDB,
|
||||
Tracer: tracer,
|
||||
Reg: reg,
|
||||
Tracer: opts.Tracer,
|
||||
Reg: opts.Reg,
|
||||
IsHA: isHA,
|
||||
withPruner: withPruner,
|
||||
storageMetrics: storageMetrics,
|
||||
storageMetrics: opts.StorageMetrics,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.Backend = store
|
||||
opts.Diagnostics = store
|
||||
opts.Lifecycle = store
|
||||
opts.Search = searchOptions
|
||||
opts.IndexMetrics = indexMetrics
|
||||
serverOptions.Backend = store
|
||||
serverOptions.Diagnostics = store
|
||||
serverOptions.Lifecycle = store
|
||||
serverOptions.Search = opts.SearchOptions
|
||||
serverOptions.IndexMetrics = opts.IndexMetrics
|
||||
serverOptions.QOSQueue = opts.QOSQueue
|
||||
|
||||
rs, err := resource.NewResourceServer(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
return resource.NewResourceServer(serverOptions)
|
||||
}
|
||||
|
||||
// isHighAvailabilityEnabled determines if high availability mode should
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/util/scheduler"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -50,6 +51,11 @@ type UnifiedStorageGrpcService interface {
|
||||
type service struct {
|
||||
*services.BasicService
|
||||
|
||||
// Subservices manager
|
||||
subservices *services.Manager
|
||||
subservicesWatcher *services.FailureWatcher
|
||||
hasSubservices bool
|
||||
|
||||
cfg *setting.Cfg
|
||||
features featuremgmt.FeatureToggles
|
||||
db infraDB.DB
|
||||
@@ -71,6 +77,9 @@ type service struct {
|
||||
|
||||
storageRing *ring.Ring
|
||||
lifecycler *ring.BasicLifecycler
|
||||
|
||||
queue QOSEnqueueDequeuer
|
||||
scheduler *scheduler.Scheduler
|
||||
}
|
||||
|
||||
func ProvideUnifiedStorageGrpcService(
|
||||
@@ -85,6 +94,7 @@ func ProvideUnifiedStorageGrpcService(
|
||||
storageRing *ring.Ring,
|
||||
memberlistKVConfig kv.Config,
|
||||
) (UnifiedStorageGrpcService, error) {
|
||||
var err error
|
||||
tracer := otel.Tracer("unified-storage")
|
||||
|
||||
// FIXME: This is a temporary solution while we are migrating to the new authn interceptor
|
||||
@@ -95,20 +105,22 @@ func ProvideUnifiedStorageGrpcService(
|
||||
})
|
||||
|
||||
s := &service{
|
||||
cfg: cfg,
|
||||
features: features,
|
||||
stopCh: make(chan struct{}),
|
||||
authenticator: authn,
|
||||
tracing: tracer,
|
||||
db: db,
|
||||
log: log,
|
||||
reg: reg,
|
||||
docBuilders: docBuilders,
|
||||
storageMetrics: storageMetrics,
|
||||
indexMetrics: indexMetrics,
|
||||
storageRing: storageRing,
|
||||
cfg: cfg,
|
||||
features: features,
|
||||
stopCh: make(chan struct{}),
|
||||
authenticator: authn,
|
||||
tracing: tracer,
|
||||
db: db,
|
||||
log: log,
|
||||
reg: reg,
|
||||
docBuilders: docBuilders,
|
||||
storageMetrics: storageMetrics,
|
||||
indexMetrics: indexMetrics,
|
||||
storageRing: storageRing,
|
||||
subservicesWatcher: services.NewFailureWatcher(),
|
||||
}
|
||||
|
||||
subservices := []services.Service{}
|
||||
if cfg.EnableSharding {
|
||||
ringStore, err := kv.NewClient(
|
||||
memberlistKVConfig,
|
||||
@@ -143,15 +155,50 @@ func ProvideUnifiedStorageGrpcService(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize storage-ring lifecycler: %s", err)
|
||||
}
|
||||
subservices = append(subservices, s.lifecycler)
|
||||
}
|
||||
|
||||
if cfg.QOSEnabled {
|
||||
qosReg := prometheus.WrapRegistererWithPrefix("resource_server_qos_", reg)
|
||||
queue := scheduler.NewQueue(&scheduler.QueueOptions{
|
||||
MaxSizePerTenant: cfg.QOSMaxSizePerTenant,
|
||||
Registerer: qosReg,
|
||||
})
|
||||
scheduler, err := scheduler.NewScheduler(queue, &scheduler.Config{
|
||||
NumWorkers: cfg.QOSNumberWorker,
|
||||
Logger: log,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create qos scheduler: %s", err)
|
||||
}
|
||||
|
||||
s.queue = queue
|
||||
s.scheduler = scheduler
|
||||
subservices = append(subservices, s.queue, s.scheduler)
|
||||
}
|
||||
|
||||
if len(subservices) > 0 {
|
||||
s.hasSubservices = true
|
||||
s.subservices, err = services.NewManager(subservices...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create subservices manager: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This will be used when running as a dskit service
|
||||
s.BasicService = services.NewBasicService(s.start, s.running, s.stopping).WithName(modules.StorageServer)
|
||||
s.BasicService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(modules.StorageServer)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *service) start(ctx context.Context) error {
|
||||
func (s *service) starting(ctx context.Context) error {
|
||||
if s.hasSubservices {
|
||||
s.subservicesWatcher.WatchManager(s.subservices)
|
||||
if err := services.StartManagerAndAwaitHealthy(ctx, s.subservices); err != nil {
|
||||
return fmt.Errorf("failed to start subservices: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
authzClient, err := authz.ProvideStandaloneAuthZClient(s.cfg, s.features, s.tracing)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -162,7 +209,19 @@ func (s *service) start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := NewResourceServer(s.db, s.cfg, s.tracing, s.reg, authzClient, searchOptions, s.storageMetrics, s.indexMetrics, s.features)
|
||||
serverOptions := ServerOptions{
|
||||
DB: s.db,
|
||||
Cfg: s.cfg,
|
||||
Tracer: s.tracing,
|
||||
Reg: s.reg,
|
||||
AccessClient: authzClient,
|
||||
SearchOptions: searchOptions,
|
||||
StorageMetrics: s.storageMetrics,
|
||||
IndexMetrics: s.indexMetrics,
|
||||
Features: s.features,
|
||||
QOSQueue: s.queue,
|
||||
}
|
||||
server, err := NewResourceServer(serverOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,11 +251,6 @@ func (s *service) start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if s.cfg.EnableSharding {
|
||||
err = s.lifecycler.StartAsync(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start the lifecycler: %s", err)
|
||||
}
|
||||
|
||||
s.log.Info("waiting until resource server is JOINING in the ring")
|
||||
lfcCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
@@ -231,15 +285,27 @@ func (s *service) GetAddress() string {
|
||||
func (s *service) running(ctx context.Context) error {
|
||||
select {
|
||||
case err := <-s.stoppedCh:
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
case err := <-s.subservicesWatcher.Chan():
|
||||
return fmt.Errorf("subservice failure: %w", err)
|
||||
case <-ctx.Done():
|
||||
close(s.stopCh)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) stopping(_ error) error {
|
||||
if s.hasSubservices {
|
||||
err := services.StopManagerAndAwaitStopped(context.Background(), s.subservices)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop subservices: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type authenticatorWithFallback struct {
|
||||
authenticator func(ctx context.Context) (context.Context, error)
|
||||
fallback func(ctx context.Context) (context.Context, error)
|
||||
@@ -309,14 +375,6 @@ func NewAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer, t
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) stopping(err error) error {
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
s.log.Error("stopping unified storage grpc service", "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toLifecyclerConfig(cfg *setting.Cfg, logger log.Logger) (ring.BasicLifecyclerConfig, error) {
|
||||
instanceAddr, err := ring.GetInstanceAddr(cfg.MemberlistBindAddr, netutil.PrivateNetworkInterfacesWithFallback([]string{"eth0", "en0"}, logger), logger, true)
|
||||
if err != nil {
|
||||
|
||||
@@ -1851,6 +1851,10 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"moreInfo": {
|
||||
"description": "More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.",
|
||||
"type": "string"
|
||||
},
|
||||
"severity": {
|
||||
"description": "Severity of the failure",
|
||||
"type": "string",
|
||||
|
||||
@@ -11,5 +11,5 @@ spec:
|
||||
description: This is a secret
|
||||
value: this is super duper secure
|
||||
decrypters:
|
||||
- actor_k6
|
||||
- actor_synthetic-monitoring
|
||||
- k6
|
||||
- synthetic-monitoring
|
||||
|
||||
@@ -12,5 +12,5 @@ spec:
|
||||
keeper: my-keeper-1
|
||||
value: super duper secure
|
||||
decrypters:
|
||||
- actor_k6
|
||||
- actor_synthetic-monitoring
|
||||
- k6
|
||||
- synthetic-monitoring
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
)
|
||||
|
||||
var logger = backend.NewLoggerWith("logger", "tsdb.jaeger")
|
||||
@@ -20,7 +19,7 @@ type Service struct {
|
||||
im instancemgmt.InstanceManager
|
||||
}
|
||||
|
||||
func ProvideService(httpClientProvider httpclient.Provider) *Service {
|
||||
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
|
||||
return &Service{
|
||||
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
|
||||
}
|
||||
@@ -36,7 +35,7 @@ type datasourceJSONData struct {
|
||||
} `json:"traceIdTimeParams"`
|
||||
}
|
||||
|
||||
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
httpClientOptions, err := settings.HTTPClientOptions(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
|
||||
jaeger "github.com/grafana/grafana/pkg/tsdb/jaeger"
|
||||
)
|
||||
|
||||
var (
|
||||
_ backend.QueryDataHandler = (*Datasource)(nil)
|
||||
_ backend.CheckHealthHandler = (*Datasource)(nil)
|
||||
_ backend.CallResourceHandler = (*Datasource)(nil)
|
||||
)
|
||||
|
||||
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return &Datasource{
|
||||
Service: jaeger.ProvideService(httpclient.NewProvider()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Datasource struct {
|
||||
Service *jaeger.Service
|
||||
}
|
||||
|
||||
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
return d.Service.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return d.Service.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return d.Service.CheckHealth(ctx, req)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Start listening to requests sent from Grafana. This call is blocking so
|
||||
// it won't finish until Grafana shuts down the process or the plugin choose
|
||||
// to exit by itself using os.Exit. Manage automatically manages life cycle
|
||||
// of datasource instances. It accepts datasource instance factory as first
|
||||
// argument. This factory will be automatically called on incoming request
|
||||
// from Grafana to create different instances of SampleDatasource (per datasource
|
||||
// ID). When datasource configuration changed Dispose method will be called and
|
||||
// new datasource instance created using NewSampleDatasource factory.
|
||||
if err := datasource.Manage("jaeger", NewDatasource, datasource.ManageOpts{}); err != nil {
|
||||
log.DefaultLogger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/zipkin"
|
||||
)
|
||||
|
||||
var (
|
||||
_ backend.QueryDataHandler = (*Datasource)(nil)
|
||||
_ backend.CheckHealthHandler = (*Datasource)(nil)
|
||||
_ backend.CallResourceHandler = (*Datasource)(nil)
|
||||
)
|
||||
|
||||
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return &Datasource{
|
||||
Service: zipkin.ProvideService(httpclient.NewProvider()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Datasource struct {
|
||||
Service *zipkin.Service
|
||||
}
|
||||
|
||||
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
return d.Service.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return d.Service.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return d.Service.CheckHealth(ctx, req)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Start listening to requests sent from Grafana. This call is blocking so
|
||||
// it won't finish until Grafana shuts down the process or the plugin choose
|
||||
// to exit by itself using os.Exit. Manage automatically manages life cycle
|
||||
// of datasource instances. It accepts datasource instance factory as first
|
||||
// argument. This factory will be automatically called on incoming request
|
||||
// from Grafana to create different instances of SampleDatasource (per datasource
|
||||
// ID). When datasource configuration changed Dispose method will be called and
|
||||
// new datasource instance created using NewSampleDatasource factory.
|
||||
if err := datasource.Manage("zipkin", NewDatasource, datasource.ManageOpts{}); err != nil {
|
||||
log.DefaultLogger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
)
|
||||
|
||||
var logger = backend.NewLoggerWith("logger", "tsdb.zipkin")
|
||||
@@ -19,7 +18,7 @@ type Service struct {
|
||||
im instancemgmt.InstanceManager
|
||||
}
|
||||
|
||||
func ProvideService(httpClientProvider httpclient.Provider) *Service {
|
||||
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
|
||||
return &Service{
|
||||
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
|
||||
}
|
||||
@@ -29,7 +28,7 @@ type datasourceInfo struct {
|
||||
ZipkinClient ZipkinClient
|
||||
}
|
||||
|
||||
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
httpClientOptions, err := settings.HTTPClientOptions(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/grafana/dskit/services"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -82,6 +84,8 @@ func NewNoopQueue() *NoopQueue {
|
||||
type Queue struct {
|
||||
services.Service
|
||||
|
||||
logger log.Logger
|
||||
|
||||
enqueueChan chan enqueueRequest
|
||||
dequeueChan chan dequeueRequest
|
||||
lenChan chan lenRequest
|
||||
@@ -108,6 +112,7 @@ type Queue struct {
|
||||
type QueueOptions struct {
|
||||
MaxSizePerTenant int
|
||||
Registerer prometheus.Registerer
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
// NewQueue creates a new Queue and starts its dispatcher goroutine.
|
||||
@@ -116,7 +121,13 @@ func NewQueue(opts *QueueOptions) *Queue {
|
||||
opts.MaxSizePerTenant = DefaultMaxSizePerTenant
|
||||
}
|
||||
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = log.NewNopLogger()
|
||||
}
|
||||
|
||||
q := &Queue{
|
||||
logger: opts.Logger,
|
||||
|
||||
enqueueChan: make(chan enqueueRequest),
|
||||
dequeueChan: make(chan dequeueRequest),
|
||||
lenChan: make(chan lenRequest),
|
||||
@@ -226,6 +237,8 @@ func (q *Queue) handleLenRequest(req lenRequest) {
|
||||
func (q *Queue) dispatcherLoop(ctx context.Context) error {
|
||||
defer close(q.dispatcherStoppedChan)
|
||||
|
||||
q.logger.Info("queue running", "maxSizePerTenant", q.maxSizePerTenant)
|
||||
|
||||
for {
|
||||
q.scheduleRoundRobin()
|
||||
|
||||
@@ -275,7 +288,6 @@ func (q *Queue) Enqueue(ctx context.Context, tenantID string, runnable func(ctx
|
||||
select {
|
||||
case q.enqueueChan <- req:
|
||||
err = <-respChan
|
||||
q.enqueueDuration.Observe(time.Since(start).Seconds())
|
||||
case <-q.dispatcherStoppedChan:
|
||||
q.discardedRequests.WithLabelValues(tenantID, "dispatcher_stopped").Inc()
|
||||
err = ErrQueueClosed
|
||||
@@ -283,6 +295,7 @@ func (q *Queue) Enqueue(ctx context.Context, tenantID string, runnable func(ctx
|
||||
q.discardedRequests.WithLabelValues(tenantID, "context_canceled").Inc()
|
||||
err = ctx.Err()
|
||||
}
|
||||
q.enqueueDuration.Observe(time.Since(start).Seconds())
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -352,6 +365,8 @@ func (q *Queue) ActiveTenantsLen() int {
|
||||
}
|
||||
|
||||
func (q *Queue) stopping(_ error) error {
|
||||
q.logger.Info("queue stopping")
|
||||
|
||||
q.queueLength.Reset()
|
||||
q.discardedRequests.Reset()
|
||||
for _, tq := range q.tenantQueues {
|
||||
@@ -359,5 +374,7 @@ func (q *Queue) stopping(_ error) error {
|
||||
}
|
||||
q.activeTenants.Init()
|
||||
q.pendingDequeueRequests.Init()
|
||||
|
||||
q.logger.Info("queue stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/dskit/services"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -25,6 +26,9 @@ func QueueOptionsWithDefaults(opts *QueueOptions) *QueueOptions {
|
||||
if opts.Registerer == nil {
|
||||
opts.Registerer = prometheus.NewRegistry()
|
||||
}
|
||||
if opts.Logger == nil {
|
||||
opts.Logger = log.New("qos.test")
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -130,16 +131,16 @@ func TestScheduler(t *testing.T) {
|
||||
t.Run("ProcessItems", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
q := NewQueue(QueueOptionsWithDefaults(nil))
|
||||
q := NewQueue(QueueOptionsWithDefaults(&QueueOptions{MaxSizePerTenant: 1000}))
|
||||
require.NoError(t, services.StartAndAwaitRunning(context.Background(), q))
|
||||
|
||||
const itemCount = 10
|
||||
const itemCount = 1000
|
||||
var processed sync.Map
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(itemCount)
|
||||
|
||||
scheduler, err := NewScheduler(q, &Config{
|
||||
NumWorkers: 2,
|
||||
NumWorkers: 10,
|
||||
MaxBackoff: 100 * time.Millisecond,
|
||||
Logger: log.New("qos.test"),
|
||||
})
|
||||
@@ -148,8 +149,11 @@ func TestScheduler(t *testing.T) {
|
||||
|
||||
for i := 0; i < itemCount; i++ {
|
||||
itemID := i
|
||||
require.NoError(t, q.Enqueue(context.Background(), "tenant-1", func(_ context.Context) {
|
||||
tenantIndex := itemID % 10
|
||||
tenantID := fmt.Sprintf("tenant-%d", tenantIndex)
|
||||
require.NoError(t, q.Enqueue(context.Background(), tenantID, func(_ context.Context) {
|
||||
processed.Store(itemID, true)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
wg.Done()
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user