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 }