github.com/weaviate/weaviate@v1.24.6/test/acceptance/graphql_resolvers/metrics_stability_test.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package test
    13  
    14  import (
    15  	"bufio"
    16  	"context"
    17  	"fmt"
    18  	"math"
    19  	"math/rand"
    20  	"net/http"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  	"github.com/weaviate/weaviate/client/backups"
    28  	"github.com/weaviate/weaviate/client/objects"
    29  	"github.com/weaviate/weaviate/entities/models"
    30  	"github.com/weaviate/weaviate/entities/schema"
    31  	"github.com/weaviate/weaviate/test/helper"
    32  	graphqlhelper "github.com/weaviate/weaviate/test/helper/graphql"
    33  )
    34  
    35  const metricClassPrefix = "MetricsClassPrefix"
    36  
    37  func metricsCount(t *testing.T) {
    38  	defer cleanupMetricsClasses(t, 0, 20)
    39  	createImportQueryMetricsClasses(t, 0, 10)
    40  	backupID := startBackup(t, 0, 10)
    41  	waitForBackupToFinish(t, backupID)
    42  	metricsLinesBefore := countMetricsLines(t)
    43  	createImportQueryMetricsClasses(t, 10, 20)
    44  	backupID = startBackup(t, 0, 20)
    45  	waitForBackupToFinish(t, backupID)
    46  	metricsLinesAfter := countMetricsLines(t)
    47  	assert.Equal(t, metricsLinesBefore, metricsLinesAfter, "number of metrics should not have changed")
    48  }
    49  
    50  func createImportQueryMetricsClasses(t *testing.T, start, end int) {
    51  	for i := start; i < end; i++ {
    52  		createMetricsClass(t, i)
    53  		importMetricsClass(t, i)
    54  		queryMetricsClass(t, i)
    55  	}
    56  }
    57  
    58  func createMetricsClass(t *testing.T, classIndex int) {
    59  	createObjectClass(t, &models.Class{
    60  		Class:      metricsClassName(classIndex),
    61  		Vectorizer: "none",
    62  		Properties: []*models.Property{
    63  			{
    64  				Name:     "some_text",
    65  				DataType: schema.DataTypeText.PropString(),
    66  			},
    67  		},
    68  		VectorIndexConfig: map[string]any{
    69  			"efConstruction": 10,
    70  			"maxConnextions": 2,
    71  			"ef":             10,
    72  		},
    73  	})
    74  }
    75  
    76  func queryMetricsClass(t *testing.T, classIndex int) {
    77  	// object by ID which exists
    78  	resp, err := helper.Client(t).Objects.
    79  		ObjectsClassGet(
    80  			objects.NewObjectsClassGetParams().
    81  				WithID(helper.IntToUUID(1)).
    82  				WithClassName(metricsClassName(classIndex)),
    83  			nil)
    84  
    85  	require.Nil(t, err)
    86  	assert.NotNil(t, resp.Payload)
    87  
    88  	// object by ID which doesn't exist
    89  	// ignore any return values
    90  	helper.Client(t).Objects.
    91  		ObjectsClassGet(
    92  			objects.NewObjectsClassGetParams().
    93  				WithID(helper.IntToUUID(math.MaxUint64)).
    94  				WithClassName(metricsClassName(classIndex)),
    95  			nil)
    96  
    97  	// vector search
    98  	result := graphqlhelper.AssertGraphQL(t, helper.RootAuth,
    99  		fmt.Sprintf(
   100  			"{  Get { %s(nearVector:{vector: [0.3,0.3,0.7,0.7]}, limit:5) { some_text } } }",
   101  			metricsClassName(classIndex),
   102  		),
   103  	)
   104  	objs := result.Get("Get", metricsClassName(classIndex)).AsSlice()
   105  	assert.Len(t, objs, 5)
   106  
   107  	// filtered vector search (which has specific metrics)
   108  	// vector search
   109  	result = graphqlhelper.AssertGraphQL(t, helper.RootAuth,
   110  		fmt.Sprintf(
   111  			"{  Get { %s(nearVector:{vector:[0.3,0.3,0.7,0.7]}, limit:5, where: %s) { some_text } } }",
   112  			metricsClassName(classIndex),
   113  			`{operator:Equal, valueText: "individually", path:["some_text"]}`,
   114  		),
   115  	)
   116  	objs = result.Get("Get", metricsClassName(classIndex)).AsSlice()
   117  	assert.Len(t, objs, 1)
   118  }
   119  
   120  // make sure that we use both individual as well as batch imports, as they
   121  // might produce different metrics
   122  func importMetricsClass(t *testing.T, classIndex int) {
   123  	// individual
   124  	createObject(t, &models.Object{
   125  		Class: metricsClassName(classIndex),
   126  		Properties: map[string]interface{}{
   127  			"some_text": "this object was created individually",
   128  		},
   129  		ID:     helper.IntToUUID(1),
   130  		Vector: randomVector(4),
   131  	})
   132  
   133  	// with batches
   134  	const (
   135  		batchSize  = 100
   136  		numBatches = 50
   137  	)
   138  
   139  	for i := 0; i < numBatches; i++ {
   140  		batch := make([]*models.Object, batchSize)
   141  		for j := 0; j < batchSize; j++ {
   142  			batch[j] = &models.Object{
   143  				Class: metricsClassName(classIndex),
   144  				Properties: map[string]interface{}{
   145  					"some_text": fmt.Sprintf("this is object %d of batch %d", j, i),
   146  				},
   147  				Vector: randomVector(4),
   148  			}
   149  		}
   150  
   151  		createObjectsBatch(t, batch)
   152  	}
   153  }
   154  
   155  func cleanupMetricsClasses(t *testing.T, start, end int) {
   156  	for i := start; i < end; i++ {
   157  		deleteObjectClass(t, metricsClassName(i))
   158  	}
   159  }
   160  
   161  func randomVector(dims int) []float32 {
   162  	out := make([]float32, dims)
   163  	for i := range out {
   164  		out[i] = rand.Float32()
   165  	}
   166  	return out
   167  }
   168  
   169  func countMetricsLines(t *testing.T) int {
   170  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   171  	defer cancel()
   172  
   173  	req, err := http.NewRequestWithContext(ctx, http.MethodGet,
   174  		"http://localhost:2112/metrics", nil)
   175  	require.Nil(t, err)
   176  
   177  	c := &http.Client{}
   178  	res, err := c.Do(req)
   179  	require.Nil(t, err)
   180  
   181  	defer res.Body.Close()
   182  	require.Equal(t, http.StatusOK, res.StatusCode)
   183  
   184  	scanner := bufio.NewScanner(res.Body)
   185  	lineCount := 0
   186  	for scanner.Scan() {
   187  		line := scanner.Text()
   188  		if strings.Contains(line, "shards_loaded") || strings.Contains(line, "shards_loading") || strings.Contains(line, "shards_unloading") || strings.Contains(line, "shards_unloaded") {
   189  			continue
   190  		}
   191  		require.NotContains(
   192  			t,
   193  			strings.ToLower(line),
   194  			strings.ToLower(metricClassPrefix),
   195  		)
   196  		lineCount++
   197  	}
   198  
   199  	require.Nil(t, scanner.Err())
   200  
   201  	return lineCount
   202  }
   203  
   204  func metricsClassName(classIndex int) string {
   205  	return fmt.Sprintf("%s_%d", metricClassPrefix, classIndex)
   206  }
   207  
   208  func startBackup(t *testing.T, start, end int) string {
   209  	var includeClasses []string
   210  	for i := start; i < end; i++ {
   211  		includeClasses = append(includeClasses, metricsClassName(i))
   212  	}
   213  
   214  	backupID := fmt.Sprintf("metrics-test-backup-%d", rand.Intn(100000000))
   215  
   216  	_, err := helper.Client(t).Backups.BackupsCreate(
   217  		backups.NewBackupsCreateParams().
   218  			WithBackend("filesystem").
   219  			WithBody(&models.BackupCreateRequest{
   220  				ID:      backupID,
   221  				Include: includeClasses,
   222  			}),
   223  		nil)
   224  	require.Nil(t, err)
   225  
   226  	return backupID
   227  }
   228  
   229  func waitForBackupToFinish(t *testing.T, id string) {
   230  	getStatus := func() *backups.BackupsCreateStatusOK {
   231  		res, err := helper.Client(t).Backups.BackupsCreateStatus(
   232  			backups.NewBackupsCreateStatusParams().WithBackend("filesystem").WithID(id),
   233  			nil)
   234  		require.Nil(t, err)
   235  		return res
   236  	}
   237  	assert.EventuallyWithT(t, func(t *assert.CollectT) {
   238  		res := getStatus()
   239  		require.NotNil(t, res.Payload.Status)
   240  		assert.Equal(t, "SUCCESS", *res.Payload.Status)
   241  	}, 10*time.Minute, 1*time.Second)
   242  }