From 991b45a361eb7a3eacaf1bda85e211108ecc014f Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 28 Nov 2023 13:40:22 +0100 Subject: [PATCH 01/15] stores: move having clause into subquery --- stores/metadata.go | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index 18e3bf7eb..fe607e851 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math" + "regexp" "strings" "time" "unicode/utf8" @@ -1103,6 +1104,11 @@ func (s *SQLStore) SearchObjects(ctx context.Context, bucket, substring string, return objects, nil } +func replaceAnyValue(query string) string { + re := regexp.MustCompile(`ANY_VALUE\((.*?)\)`) + return re.ReplaceAllString(query, "$1") +} + func (s *SQLStore) ObjectEntries(ctx context.Context, bucket, path, prefix, sortBy, sortDir, marker string, offset, limit int) (metadata []api.ObjectMetadata, hasMore bool, err error) { // sanity check we are passing a directory if !strings.HasSuffix(path, "/") { @@ -1156,34 +1162,41 @@ func (s *SQLStore) ObjectEntries(ctx context.Context, bucket, path, prefix, sort // build objects query & parameters objectsQuery := fmt.Sprintf(` SELECT - MAX(etag) AS ETag, - MAX(created_at) AS ModTime, - CASE slashindex WHEN 0 THEN %s ELSE %s END AS name, + ANY_VALUE(etag) AS ETag, + ANY_VALUE(created_at) AS ModTime, + ANY_VALUE(name) AS name, SUM(size) AS size, MIN(health) as health, - MAX(mimeType) as MimeType + ANY_VALUE(mimeType) as MimeType FROM ( - SELECT MAX(etag) AS etag, MAX(objects.created_at) AS created_at, MAX(size) AS size, MIN(slabs.health) as health, MAX(objects.mime_type) as mimeType, SUBSTR(object_id, ?) AS trimmed , INSTR(SUBSTR(object_id, ?), "/") AS slashindex + SELECT ANY_VALUE(etag) AS etag, + ANY_VALUE(objects.created_at) AS created_at, + ANY_VALUE(size) AS size, + MIN(slabs.health) as health, + ANY_VALUE(objects.mime_type) as mimeType, + CASE INSTR(SUBSTR(object_id, ?), "/") WHEN 0 THEN %s ELSE %s END AS oname FROM objects INNER JOIN buckets b ON objects.db_bucket_id = b.id LEFT JOIN slices ON objects.id = slices.db_object_id LEFT JOIN slabs ON slices.db_slab_id = slabs.id - WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? + WHERE SUBSTR(object_id, 1, ?) = ? GROUP BY object_id + HAVING SUBSTR(oname, 1, ?) = ? AND oname != ? ) AS m GROUP BY name -HAVING SUBSTR(name, 1, ?) = ? AND name != ? `, - sqlConcat(s.db, "?", "trimmed"), - sqlConcat(s.db, "?", "substr(trimmed, 1, slashindex)"), + sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), + sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), '/'))"), ) - objectsQueryParams := []interface{}{ - path, // sqlConcat(s.db, "?", "trimmed"), - path, // sqlConcat(s.db, "?", "substr(trimmed, 1, slashindex)") + if isSQLite(s.db) { + objectsQuery = replaceAnyValue(objectsQuery) + } - utf8.RuneCountInString(path) + 1, // SUBSTR(object_id, ?) - utf8.RuneCountInString(path) + 1, // INSTR(SUBSTR(object_id, ?), "/") + objectsQueryParams := []interface{}{ + utf8.RuneCountInString(path) + 1, // CASE INSTRU(SUBSTR(object_id, ?), "/") + path, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), + path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), "/"))") utf8.RuneCountInString(path), // WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? path, // WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? From 5d88c53631570a6ed2aa2e350af18d02d3d1d6f8 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 28 Nov 2023 15:55:23 +0100 Subject: [PATCH 02/15] stores: add health to objects --- stores/metadata.go | 64 ++++++++++++++++------------------------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index fe607e851..03bf0950f 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -107,9 +107,10 @@ type ( DBBucket dbBucket ObjectID string `gorm:"index;uniqueIndex:idx_object_bucket"` - Key secretKey - Slabs []dbSlice `gorm:"constraint:OnDelete:CASCADE"` // CASCADE to delete slices too - Size int64 + Key secretKey + Slabs []dbSlice `gorm:"constraint:OnDelete:CASCADE"` // CASCADE to delete slices too + Health float64 `gorm:"index;default:1.0; NOT NULL"` + Size int64 MimeType string `json:"index"` Etag string `gorm:"index"` @@ -459,8 +460,6 @@ func (raw rawObject) convert() (api.Object, error) { if err := addSlab(len(filtered)); err != nil { return api.Object{}, err } - } else { - minHealth = 1 // empty object } // fetch a potential partial slab from the buffer. @@ -481,7 +480,7 @@ func (raw rawObject) convert() (api.Object, error) { return api.Object{ ObjectMetadata: api.ObjectMetadata{ ETag: raw[0].ObjectETag, - Health: minHealth, + Health: raw[0].ObjectHealth, MimeType: raw[0].ObjectMimeType, ModTime: raw[0].ObjectModTime.UTC(), Name: raw[0].ObjectName, @@ -1161,29 +1160,16 @@ func (s *SQLStore) ObjectEntries(ctx context.Context, bucket, path, prefix, sort // build objects query & parameters objectsQuery := fmt.Sprintf(` -SELECT - ANY_VALUE(etag) AS ETag, - ANY_VALUE(created_at) AS ModTime, - ANY_VALUE(name) AS name, - SUM(size) AS size, - MIN(health) as health, - ANY_VALUE(mimeType) as MimeType -FROM ( - SELECT ANY_VALUE(etag) AS etag, - ANY_VALUE(objects.created_at) AS created_at, - ANY_VALUE(size) AS size, - MIN(slabs.health) as health, - ANY_VALUE(objects.mime_type) as mimeType, - CASE INSTR(SUBSTR(object_id, ?), "/") WHEN 0 THEN %s ELSE %s END AS oname - FROM objects - INNER JOIN buckets b ON objects.db_bucket_id = b.id - LEFT JOIN slices ON objects.id = slices.db_object_id - LEFT JOIN slabs ON slices.db_slab_id = slabs.id - WHERE SUBSTR(object_id, 1, ?) = ? - GROUP BY object_id - HAVING SUBSTR(oname, 1, ?) = ? AND oname != ? -) AS m -GROUP BY name +SELECT ANY_VALUE(etag) AS ETag, +MAX(created_at) AS ModTime, +CASE INSTR(SUBSTR(object_id, ?), "/") WHEN 0 THEN %s ELSE %s END AS Name +SUM(size) AS size, +MIN(health) as health, +ANY_VALUE(mimeType) as MimeType +FROM objects +INNER JOIN buckets b ON objects.db_bucket_id = b.id +WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? +GROUP BY Name `, sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), '/'))"), @@ -1198,13 +1184,9 @@ GROUP BY name path, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), "/"))") - utf8.RuneCountInString(path), // WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? - path, // WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? - bucket, // WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? - - utf8.RuneCountInString(path + prefix), // HAVING SUBSTR(name, 1, ?) = ? AND name != ? - path + prefix, // HAVING SUBSTR(name, 1, ?) = ? AND name != ? - path, // HAVING SUBSTR(name, 1, ?) = ? AND name != ? + utf8.RuneCountInString(path), // WHERE SUBSTR(Name, 1, ?) = ? AND Name = ? + path, // WHERE SUBSTR(Name, 1, ?) = ? AND Name = ? + bucket, // WHERE SUBSTR(Name, 1, ?) = ? AND Name = ? } // build marker expr @@ -2096,7 +2078,7 @@ func (s *SQLStore) object(ctx context.Context, txn *gorm.DB, bucket string, path // accordingly var rows rawObject tx := s.db. - Select("o.id as ObjectID, o.key as ObjectKey, o.object_id as ObjectName, o.size as ObjectSize, o.mime_type as ObjectMimeType, o.created_at as ObjectModTime, o.etag as ObjectETag, sli.object_index, sli.offset as SliceOffset, sli.length as SliceLength, sla.id as SlabID, sla.health as SlabHealth, sla.key as SlabKey, sla.min_shards as SlabMinShards, bs.id IS NOT NULL AS SlabBuffered, sec.slab_index as SectorIndex, sec.root as SectorRoot, sec.latest_host as LatestHost, c.fcid as FCID, h.public_key as HostKey"). + Select("o.id as ObjectID, o.health as ObjectHealth, o.key as ObjectKey, o.object_id as ObjectName, o.size as ObjectSize, o.mime_type as ObjectMimeType, o.created_at as ObjectModTime, o.etag as ObjectETag, sli.object_index, sli.offset as SliceOffset, sli.length as SliceLength, sla.id as SlabID, sla.health as SlabHealth, sla.key as SlabKey, sla.min_shards as SlabMinShards, bs.id IS NOT NULL AS SlabBuffered, sec.slab_index as SectorIndex, sec.root as SectorRoot, sec.latest_host as LatestHost, c.fcid as FCID, h.public_key as HostKey"). Model(&dbObject{}). Table("objects o"). Joins("INNER JOIN buckets b ON o.db_bucket_id = b.id"). @@ -2120,12 +2102,10 @@ func (s *SQLStore) object(ctx context.Context, txn *gorm.DB, bucket string, path func (s *SQLStore) objectHealth(ctx context.Context, tx *gorm.DB, objectID uint) (health float64, err error) { if err = tx. - Select("COALESCE(MIN(sla.health), 1)"). + Select("objects.health"). Model(&dbObject{}). - Table("objects o"). - Joins("LEFT JOIN slices sli ON o.id = sli.`db_object_id`"). - Joins("LEFT JOIN slabs sla ON sli.db_slab_id = sla.`id`"). - Where("o.id = ?", objectID). + Table("objects"). + Where("id", objectID). Scan(&health). Error; errors.Is(err, gorm.ErrRecordNotFound) { err = api.ErrObjectNotFound From 1edff5f28919f10c7c643545aae436a644e9805d Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 28 Nov 2023 17:21:06 +0100 Subject: [PATCH 03/15] stores: fix TsetObjectEntries --- stores/metadata.go | 49 ++++++++++++++++++++++++----------------- stores/metadata_test.go | 6 +++++ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index 03bf0950f..dd9a89e67 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1160,16 +1160,19 @@ func (s *SQLStore) ObjectEntries(ctx context.Context, bucket, path, prefix, sort // build objects query & parameters objectsQuery := fmt.Sprintf(` -SELECT ANY_VALUE(etag) AS ETag, -MAX(created_at) AS ModTime, -CASE INSTR(SUBSTR(object_id, ?), "/") WHEN 0 THEN %s ELSE %s END AS Name -SUM(size) AS size, -MIN(health) as health, -ANY_VALUE(mimeType) as MimeType -FROM objects -INNER JOIN buckets b ON objects.db_bucket_id = b.id -WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? -GROUP BY Name +SELECT ETag, ModTime, oname as Name, Size, Health, MimeType +FROM ( + SELECT ANY_VALUE(etag) AS ETag, + MAX(objects.created_at) AS ModTime, + CASE INSTR(SUBSTR(object_id, ?), "/") WHEN 0 THEN %s ELSE %s END AS oname, + SUM(size) AS Size, + MIN(health) as Health, + ANY_VALUE(mime_type) as MimeType + FROM objects + INNER JOIN buckets b ON objects.db_bucket_id = b.id + WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? AND SUBSTR(oname, 1, ?) = ? AND oname != ? + GROUP BY oname +) baseQuery `, sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), '/'))"), @@ -1184,9 +1187,13 @@ GROUP BY Name path, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), "/"))") - utf8.RuneCountInString(path), // WHERE SUBSTR(Name, 1, ?) = ? AND Name = ? - path, // WHERE SUBSTR(Name, 1, ?) = ? AND Name = ? - bucket, // WHERE SUBSTR(Name, 1, ?) = ? AND Name = ? + utf8.RuneCountInString(path), // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? + path, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? + bucket, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? + + utf8.RuneCountInString(path + prefix), // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? + path + prefix, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? + path, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? } // build marker expr @@ -1197,24 +1204,24 @@ GROUP BY Name case api.ObjectSortByHealth: var markerHealth float64 if err = s.db. - Raw(fmt.Sprintf(`SELECT health FROM (%s AND name >= ? ORDER BY name LIMIT 1) as n`, objectsQuery), append(objectsQueryParams, marker)...). + Raw(fmt.Sprintf(`SELECT Health FROM (%s WHERE Name >= ? ORDER BY Name LIMIT 1) as n`, objectsQuery), append(objectsQueryParams, marker)...). Scan(&markerHealth). Error; err != nil { return } if sortDir == api.ObjectSortDirAsc { - markerExpr = "(health > ? OR (health = ? AND name > ?))" + markerExpr = "(Health > ? OR (Health = ? AND Name > ?))" markerParams = []interface{}{markerHealth, markerHealth, marker} } else { - markerExpr = "(health = ? AND name > ?) OR health < ?" + markerExpr = "(Health = ? AND Name > ?) OR Health < ?" markerParams = []interface{}{markerHealth, marker, markerHealth} } case api.ObjectSortByName: if sortDir == api.ObjectSortDirAsc { - markerExpr = "name > ?" + markerExpr = "Name > ?" } else { - markerExpr = "name < ?" + markerExpr = "Name < ?" } markerParams = []interface{}{marker} default: @@ -1225,7 +1232,7 @@ GROUP BY Name // build order clause orderByClause := fmt.Sprintf("%s %s", sortBy, sortDir) if sortBy == api.ObjectSortByHealth { - orderByClause += ", name" + orderByClause += ", Name" } var rows []rawObjectMetadata @@ -1912,7 +1919,9 @@ LIMIT ? return res.Error } rowsAffected = res.RowsAffected - return nil + + // Update the health of the objects associated with the updated slabs. + return tx.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices WHERE db_slab_id IN (?))", healthQuery).Error }) if err != nil { return err diff --git a/stores/metadata_test.go b/stores/metadata_test.go index 580113b4b..5cfe18214 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -1392,6 +1392,12 @@ func TestObjectEntries(t *testing.T) { t.Fatal(err) } + // update health of objects to match the overridden health of the slabs + err := ss.db.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices)").Error + if err != nil { + t.Fatal() + } + tests := []struct { path string prefix string From bebc76ffc3f1cef6538ba6df6dc4c0b18f14d6e0 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 28 Nov 2023 17:52:20 +0100 Subject: [PATCH 04/15] stores: fix TestListObjects --- stores/metadata.go | 13 ++++--------- stores/metadata_test.go | 6 ++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index dd9a89e67..a1179406c 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1921,7 +1921,7 @@ LIMIT ? rowsAffected = res.RowsAffected // Update the health of the objects associated with the updated slabs. - return tx.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices WHERE db_slab_id IN (?))", healthQuery).Error + return tx.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices WHERE db_slab_id IN (SELECT id FROM (?)))", healthQuery).Error }) if err != nil { return err @@ -2526,16 +2526,14 @@ func (s *SQLStore) ListObjects(ctx context.Context, bucket, prefix, sortBy, sort var rows []rawObjectMetadata if err := s.db. - Select("o.object_id as Name, MAX(o.size) as Size, MIN(sla.health) as Health, MAX(o.mime_type) as mimeType, MAX(o.created_at) as ModTime"). + Select("o.object_id as Name, o.size as Size, o.health as Health, o.mime_type as mimeType, o.created_at as ModTime"). Model(&dbObject{}). Table("objects o"). Joins("INNER JOIN buckets b ON o.db_bucket_id = b.id"). - Joins("LEFT JOIN slices sli ON o.id = sli.`db_object_id`"). - Joins("LEFT JOIN slabs sla ON sli.db_slab_id = sla.`id`"). Where("b.name = ? AND ? AND ?", bucket, prefixExpr, markerExpr). - Group("o.object_id"). Order(orderBy). Order(markerOrderBy). + Order("Name ASC"). Limit(int(limit)). Scan(&rows).Error; err != nil { return api.ObjectsListResponse{}, err @@ -2679,14 +2677,11 @@ func buildMarkerExpr(db *gorm.DB, bucket, prefix, marker, sortBy, sortDir string var markerHealth float64 if marker != "" && sortBy == api.ObjectSortByHealth { if err := db. - Select("MIN(sla.health)"). + Select("o.health"). Model(&dbObject{}). Table("objects o"). Joins("INNER JOIN buckets b ON o.db_bucket_id = b.id"). - Joins("LEFT JOIN slices sli ON o.id = sli.`db_object_id`"). - Joins("LEFT JOIN slabs sla ON sli.db_slab_id = sla.`id`"). Where("b.name = ? AND ? AND ?", bucket, buildPrefixExpr(prefix), gorm.Expr("o.object_id >= ?", marker)). - Group("o.object_id"). Limit(1). Scan(&markerHealth). Error; err != nil { diff --git a/stores/metadata_test.go b/stores/metadata_test.go index 5cfe18214..45d3bfba1 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -3383,6 +3383,12 @@ func TestListObjects(t *testing.T) { t.Fatal(err) } + // update health of objects to match the overridden health of the slabs + err := ss.db.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices)").Error + if err != nil { + t.Fatal() + } + tests := []struct { prefix string sortBy string From 3f7d4aee34f27f43e3c438ea29543ead47ac5ae3 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 28 Nov 2023 18:12:19 +0100 Subject: [PATCH 05/15] storse: fix TestObjectHealth --- stores/metadata.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index a1179406c..991914a54 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1885,7 +1885,7 @@ func (s *SQLStore) RefreshHealth(ctx context.Context) error { now := time.Now() for { - healthQuery := s.db.Raw(` + healthQuery := gorm.Expr(` SELECT slabs.id, slabs.db_contract_set_id, CASE WHEN (slabs.min_shards = slabs.total_shards) THEN CASE WHEN (COUNT(DISTINCT(CASE WHEN cs.name IS NULL THEN NULL ELSE c.host_id END)) < slabs.min_shards) @@ -1909,11 +1909,18 @@ LIMIT ? s.objectsMu.Lock() defer s.objectsMu.Unlock() + // create temp table from the health query since we will reuse it + if err := tx.Exec("DROP TABLE IF EXISTS src").Error; err != nil { + return err + } else if err = tx.Exec("CREATE TEMP TABLE src AS ?", healthQuery).Error; err != nil { + return err + } + var res *gorm.DB if isSQLite(s.db) { - res = tx.Exec("UPDATE slabs SET health = src.health, health_valid_until = (?) FROM (?) AS src WHERE slabs.id=src.id", sqlRandomTimestamp(s.db, now, refreshHealthMinHealthValidity, refreshHealthMaxHealthValidity), healthQuery) + res = tx.Exec("UPDATE slabs SET health = src.health, health_valid_until = (?) FROM src WHERE slabs.id=src.id", sqlRandomTimestamp(s.db, now, refreshHealthMinHealthValidity, refreshHealthMaxHealthValidity)) } else { - res = tx.Exec("UPDATE slabs sla INNER JOIN (?) h ON sla.id = h.id SET sla.health = h.health, health_valid_until = (?)", healthQuery, sqlRandomTimestamp(s.db, now, refreshHealthMinHealthValidity, refreshHealthMaxHealthValidity)) + res = tx.Exec("UPDATE slabs sla INNER JOIN src h ON sla.id = h.id SET sla.health = h.health, health_valid_until = (?)", sqlRandomTimestamp(s.db, now, refreshHealthMinHealthValidity, refreshHealthMaxHealthValidity)) } if res.Error != nil { return res.Error @@ -1921,7 +1928,9 @@ LIMIT ? rowsAffected = res.RowsAffected // Update the health of the objects associated with the updated slabs. - return tx.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices WHERE db_slab_id IN (SELECT id FROM (?)))", healthQuery).Error + return tx.Exec(`UPDATE objects SET health = src.health FROM src + INNER JOIN slices ON slices.db_slab_id = src.id + WHERE slices.db_object_id = objects.id`).Error }) if err != nil { return err From 55ed4af4aab7494ad8731e92ec654059f041e6c1 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 28 Nov 2023 18:43:12 +0100 Subject: [PATCH 06/15] storse: fix TestSQLMetadataStore --- stores/metadata_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stores/metadata_test.go b/stores/metadata_test.go index 45d3bfba1..d18a3e0d5 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -972,6 +972,7 @@ func TestSQLMetadataStore(t *testing.T) { one := uint(1) expectedObj := dbObject{ DBBucketID: 1, + Health: 1, ObjectID: objID, Key: obj1Key, Size: obj1.TotalSize(), From 44dfc39eafd37b79f1d4d69c9ebb51d2f31ff444 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 10:15:54 +0100 Subject: [PATCH 07/15] stores: fix MySQL TestObjectEntries --- stores/metadata.go | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index 991914a54..3b8cb9e65 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1158,24 +1158,30 @@ func (s *SQLStore) ObjectEntries(ctx context.Context, bucket, path, prefix, sort offset = 0 } + onameExpr := fmt.Sprintf("CASE INSTR(SUBSTR(object_id, ?), '/') WHEN 0 THEN %s ELSE %s END", + sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), + sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), '/'))"), + ) + // build objects query & parameters objectsQuery := fmt.Sprintf(` SELECT ETag, ModTime, oname as Name, Size, Health, MimeType FROM ( SELECT ANY_VALUE(etag) AS ETag, MAX(objects.created_at) AS ModTime, - CASE INSTR(SUBSTR(object_id, ?), "/") WHEN 0 THEN %s ELSE %s END AS oname, + %s AS oname, SUM(size) AS Size, MIN(health) as Health, ANY_VALUE(mime_type) as MimeType FROM objects INNER JOIN buckets b ON objects.db_bucket_id = b.id - WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? AND SUBSTR(oname, 1, ?) = ? AND oname != ? + WHERE SUBSTR(object_id, 1, ?) = ? AND b.name = ? AND SUBSTR(%s, 1, ?) = ? AND %s != ? GROUP BY oname ) baseQuery `, - sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), - sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), '/'))"), + onameExpr, + onameExpr, + onameExpr, ) if isSQLite(s.db) { @@ -1183,17 +1189,24 @@ FROM ( } objectsQueryParams := []interface{}{ - utf8.RuneCountInString(path) + 1, // CASE INSTRU(SUBSTR(object_id, ?), "/") - path, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "SUBSTR(object_id, ?)"), - path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // sqlConcat(s.db, "?", "substr(SUBSTR(object_id, ?), 1, INSTR(SUBSTR(object_id, ?), "/"))") - - utf8.RuneCountInString(path), // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? - path, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? - bucket, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? - - utf8.RuneCountInString(path + prefix), // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? - path + prefix, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? - path, // WHERE SUBSTR(Name, 1, ?) = ? AND Name != ? AND b.name = ? + utf8.RuneCountInString(path) + 1, // onameExpr + path, utf8.RuneCountInString(path) + 1, // onameExpr + path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // onameExpr + + utf8.RuneCountInString(path), // WHERE SUBSTR(%s, 1, ?) = ? AND %s != ? AND b.name = ? + path, // WHERE SUBSTR(%s, 1, ?) = ? AND %s != ? AND b.name = ? + bucket, // WHERE SUBSTR(%s, 1, ?) = ? AND %s != ? AND b.name = ? + + utf8.RuneCountInString(path) + 1, // onameExpr + path, utf8.RuneCountInString(path) + 1, // onameExpr + path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // onameExpr + + utf8.RuneCountInString(path + prefix), // WHERE SUBSTR(%s, 1, ?) = ? AND %s != ? AND b.name = ? + path + prefix, // WHERE SUBSTR(%s, 1, ?) = ? AND %s != ? AND b.name = ? + utf8.RuneCountInString(path) + 1, // onameExpr + path, utf8.RuneCountInString(path) + 1, // onameExpr + path, utf8.RuneCountInString(path) + 1, utf8.RuneCountInString(path) + 1, // onameExpr + path, // WHERE SUBSTR(%s, 1, ?) = ? AND %s != ? AND b.name = ? } // build marker expr From d9e0ebb92bdfe5d775e0ec35ce81f781055fcdeb Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 10:34:12 +0100 Subject: [PATCH 08/15] stores: add updateAllObjectsHealth helper --- stores/metadata.go | 6 +++++- stores/metadata_test.go | 6 ++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index 3b8cb9e65..5030b8876 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1898,7 +1898,7 @@ func (s *SQLStore) RefreshHealth(ctx context.Context) error { now := time.Now() for { - healthQuery := gorm.Expr(` + healthQuery := s.db.Raw(` SELECT slabs.id, slabs.db_contract_set_id, CASE WHEN (slabs.min_shards = slabs.total_shards) THEN CASE WHEN (COUNT(DISTINCT(CASE WHEN cs.name IS NULL THEN NULL ELSE c.host_id END)) < slabs.min_shards) @@ -2747,6 +2747,10 @@ func buildPrefixExpr(prefix string) clause.Expr { } } +func updateAllObjectsHealth(tx *gorm.DB) error { + return tx.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices)").Error +} + func validateSort(sortBy, sortDir string) error { allowed := func(s string, allowed ...string) bool { for _, a := range allowed { diff --git a/stores/metadata_test.go b/stores/metadata_test.go index d18a3e0d5..75294fbb1 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -1394,8 +1394,7 @@ func TestObjectEntries(t *testing.T) { } // update health of objects to match the overridden health of the slabs - err := ss.db.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices)").Error - if err != nil { + if err := updateAllObjectsHealth(ss.db); err != nil { t.Fatal() } @@ -3385,8 +3384,7 @@ func TestListObjects(t *testing.T) { } // update health of objects to match the overridden health of the slabs - err := ss.db.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices)").Error - if err != nil { + if err := updateAllObjectsHealth(ss.db); err != nil { t.Fatal() } From cc0082f3ab5114a41c3f1468aa7e8fedaed56ae5 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 11:59:58 +0100 Subject: [PATCH 09/15] stores: change TEMP to TEMPORARY for MySQL --- autopilot/migrator.go | 2 +- stores/metadata.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autopilot/migrator.go b/autopilot/migrator.go index e9e44d8aa..d1002ce47 100644 --- a/autopilot/migrator.go +++ b/autopilot/migrator.go @@ -198,7 +198,7 @@ OUTER: start := time.Now() if err := b.RefreshHealth(ctx); err != nil { m.ap.RegisterAlert(ctx, newRefreshHealthFailedAlert(err)) - m.logger.Errorf("failed to recompute cached health before migration", err) + m.logger.Errorf("failed to recompute cached health before migration: %v", err) return } m.logger.Debugf("recomputed slab health in %v", time.Since(start)) diff --git a/stores/metadata.go b/stores/metadata.go index 5030b8876..4254aef85 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1925,7 +1925,7 @@ LIMIT ? // create temp table from the health query since we will reuse it if err := tx.Exec("DROP TABLE IF EXISTS src").Error; err != nil { return err - } else if err = tx.Exec("CREATE TEMP TABLE src AS ?", healthQuery).Error; err != nil { + } else if err = tx.Exec("CREATE TEMPORARY TABLE src AS ?", healthQuery).Error; err != nil { return err } From a376213a08841fd94882341fea05dda168b2c5b9 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 12:58:32 +0100 Subject: [PATCH 10/15] stores: fix RefreshHealth for MySQL --- stores/metadata.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/stores/metadata.go b/stores/metadata.go index 4254aef85..fcfc4cfb4 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1941,9 +1941,16 @@ LIMIT ? rowsAffected = res.RowsAffected // Update the health of the objects associated with the updated slabs. - return tx.Exec(`UPDATE objects SET health = src.health FROM src + if isSQLite(s.db) { + return tx.Exec(`UPDATE objects SET health = src.health FROM src INNER JOIN slices ON slices.db_slab_id = src.id WHERE slices.db_object_id = objects.id`).Error + } else { + return tx.Exec(`UPDATE objects + INNER JOIN slices sli ON sli.db_object_id = objects.id + INNER JOIN src s ON s.id = sli.db_slab_id + SET objects.health = s.health`).Error + } }) if err != nil { return err From 855c16880e31257731898a54beb73e455756a066 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 13:53:39 +0100 Subject: [PATCH 11/15] stores: add migration code --- stores/migrations.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/stores/migrations.go b/stores/migrations.go index b3302e44e..603d70a0a 100644 --- a/stores/migrations.go +++ b/stores/migrations.go @@ -348,6 +348,12 @@ func performMigrations(db *gorm.DB, logger *zap.SugaredLogger) error { return performMigration00033_transactionsTimestampIndex(tx, logger) }, }, + { + ID: "00034_objectHealth", + Migrate: func(tx *gorm.DB) error { + return performMigration00034_objectHealth(tx, logger) + }, + }, } // Create migrator. m := gormigrate.New(db, gormigrate.DefaultOptions, migrations) @@ -1448,3 +1454,22 @@ func performMigration00033_transactionsTimestampIndex(txn *gorm.DB, logger *zap. logger.Info("migration 00033_transactionsTimestampIndex complete") return nil } + +func performMigration00034_objectHealth(txn *gorm.DB, logger *zap.SugaredLogger) error { + logger.Info("performing migration 00034_objectHealth") + + // add health column + if err := txn.Table("objects").Migrator().AutoMigrate(&struct { + Health float64 `gorm:"index;default:1.0; NOT NULL"` + }{}); err != nil { + return err + } + + // update health for all objects + if err := updateAllObjectsHealth(txn); err != nil { + return err + } + + logger.Info("migration 00034_objectHealth complete") + return nil +} From ce5e68dedf811dd44802f01fe225b4f1ed278636 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 14:38:26 +0100 Subject: [PATCH 12/15] stores: fix updateAllObjectsHealth query --- stores/metadata.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index 74698bf21..dcdbae607 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -1085,12 +1085,10 @@ func (s *SQLStore) SearchObjects(ctx context.Context, bucket, substring string, var objects []api.ObjectMetadata err := s.db. - Select("o.object_id as name, MAX(o.size) as size, MIN(sla.health) as health"). + Select("o.object_id as name, MAX(o.size) as size, o.health as health"). Model(&dbObject{}). Table("objects o"). Joins("INNER JOIN buckets b ON o.db_bucket_id = b.id"). - Joins("LEFT JOIN slices sli ON o.id = sli.`db_object_id`"). - Joins("LEFT JOIN slabs sla ON sli.db_slab_id = sla.`id`"). Where("INSTR(o.object_id, ?) > 0 AND b.name = ?", substring, bucket). Group("o.object_id"). Offset(offset). @@ -2758,7 +2756,14 @@ func buildPrefixExpr(prefix string) clause.Expr { } func updateAllObjectsHealth(tx *gorm.DB) error { - return tx.Exec("UPDATE objects SET health = (SELECT MIN(health) FROM slabs WHERE slabs.id IN (SELECT db_slab_id FROM slices WHERE db_object_id = objects.id)) WHERE id IN (SELECT DISTINCT(db_object_id) FROM slices)").Error + return tx.Exec(` +UPDATE objects +SET health = ( + SELECT COALESCE(MIN(slabs.health), 1) + FROM slabs + INNER JOIN slices sli ON sli.db_slab_id = slabs.id + WHERE sli.db_object_id = objects.id) +`).Error } func validateSort(sortBy, sortDir string) error { From a4fb0e8dcc469667b91e17629abe791f13b27d8f Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 14:44:27 +0100 Subject: [PATCH 13/15] stores: extend ObjectStats endpoint with min health --- api/object.go | 9 +++++---- internal/testing/cluster_test.go | 12 ++++++++++++ stores/metadata.go | 4 +++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/api/object.go b/api/object.go index f525d872d..ae9743589 100644 --- a/api/object.go +++ b/api/object.go @@ -114,10 +114,11 @@ type ( // ObjectsStatsResponse is the response type for the /bus/stats/objects endpoint. ObjectsStatsResponse struct { - NumObjects uint64 `json:"numObjects"` // number of objects - TotalObjectsSize uint64 `json:"totalObjectsSize"` // size of all objects - TotalSectorsSize uint64 `json:"totalSectorsSize"` // uploaded size of all objects - TotalUploadedSize uint64 `json:"totalUploadedSize"` // uploaded size of all objects including redundant sectors + NumObjects uint64 `json:"numObjects"` // number of objects + MinHealth float64 `json:"minHealth"` // minimum health of all objects + TotalObjectsSize uint64 `json:"totalObjectsSize"` // size of all objects + TotalSectorsSize uint64 `json:"totalSectorsSize"` // uploaded size of all objects + TotalUploadedSize uint64 `json:"totalUploadedSize"` // uploaded size of all objects including redundant sectors } ) diff --git a/internal/testing/cluster_test.go b/internal/testing/cluster_test.go index dd9c0f1c2..2af10eb17 100644 --- a/internal/testing/cluster_test.go +++ b/internal/testing/cluster_test.go @@ -716,6 +716,9 @@ func TestUploadDownloadExtended(t *testing.T) { if info.NumObjects != 4 { t.Error("wrong number of objects", info.NumObjects, 4) } + if info.MinHealth != 1 { + t.Errorf("expected minHealth of 1, got %v", info.MinHealth) + } // download the data for _, data := range [][]byte{small, large} { @@ -1688,6 +1691,9 @@ func TestUploadPacking(t *testing.T) { if os.TotalUploadedSize != uint64(totalRedundantSize) { return fmt.Errorf("expected totalUploadedSize of %v, got %v", totalRedundantSize, os.TotalUploadedSize) } + if os.MinHealth != 1 { + return fmt.Errorf("expected minHealth of 1, got %v", os.MinHealth) + } return nil }) @@ -1845,6 +1851,9 @@ func TestSlabBufferStats(t *testing.T) { if os.TotalUploadedSize != 0 { return fmt.Errorf("expected totalUploadedSize of 0, got %d", os.TotalUploadedSize) } + if os.MinHealth != 1 { + t.Errorf("expected minHealth of 1, got %v", os.MinHealth) + } return nil }) @@ -1897,6 +1906,9 @@ func TestSlabBufferStats(t *testing.T) { if os.TotalUploadedSize != 3*rhpv2.SectorSize { return fmt.Errorf("expected totalUploadedSize of %d, got %d", 3*rhpv2.SectorSize, os.TotalUploadedSize) } + if os.MinHealth != 1 { + t.Errorf("expected minHealth of 1, got %v", os.MinHealth) + } return nil }) diff --git a/stores/metadata.go b/stores/metadata.go index dcdbae607..6381bafb8 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -649,11 +649,12 @@ func (s *SQLStore) ObjectsStats(ctx context.Context) (api.ObjectsStatsResponse, // Number of objects. var objInfo struct { NumObjects uint64 + MinHealth float64 TotalObjectsSize uint64 } err := s.db. Model(&dbObject{}). - Select("COUNT(*) AS NumObjects, SUM(size) AS TotalObjectsSize"). + Select("COUNT(*) AS NumObjects, COALESCE(Min(health), 1) as MinHealth, SUM(size) AS TotalObjectsSize"). Scan(&objInfo). Error if err != nil { @@ -692,6 +693,7 @@ func (s *SQLStore) ObjectsStats(ctx context.Context) (api.ObjectsStatsResponse, } return api.ObjectsStatsResponse{ + MinHealth: objInfo.MinHealth, NumObjects: objInfo.NumObjects, TotalObjectsSize: objInfo.TotalObjectsSize, TotalSectorsSize: totalSectors * rhpv2.SectorSize, From 1765125e42919fe36f1f64244e501553d1630e12 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 14:51:11 +0100 Subject: [PATCH 14/15] stores: fix TestObjectsStats --- stores/metadata_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/metadata_test.go b/stores/metadata_test.go index fbdc07b60..5c8d74220 100644 --- a/stores/metadata_test.go +++ b/stores/metadata_test.go @@ -2383,7 +2383,7 @@ func TestObjectsStats(t *testing.T) { if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(info, api.ObjectsStatsResponse{}) { + if !reflect.DeepEqual(info, api.ObjectsStatsResponse{MinHealth: 1}) { t.Fatal("unexpected stats", info) } From a247c646213fb3e6802008539459fb0138b0b5d4 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 29 Nov 2023 15:28:34 +0100 Subject: [PATCH 15/15] stores: fix TestSearchObjects --- stores/metadata.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stores/metadata.go b/stores/metadata.go index 6381bafb8..f44ad599b 100644 --- a/stores/metadata.go +++ b/stores/metadata.go @@ -654,7 +654,7 @@ func (s *SQLStore) ObjectsStats(ctx context.Context) (api.ObjectsStatsResponse, } err := s.db. Model(&dbObject{}). - Select("COUNT(*) AS NumObjects, COALESCE(Min(health), 1) as MinHealth, SUM(size) AS TotalObjectsSize"). + Select("COUNT(*) AS NumObjects, COALESCE(MIN(health), 1) as MinHealth, SUM(size) AS TotalObjectsSize"). Scan(&objInfo). Error if err != nil { @@ -1087,12 +1087,12 @@ func (s *SQLStore) SearchObjects(ctx context.Context, bucket, substring string, var objects []api.ObjectMetadata err := s.db. - Select("o.object_id as name, MAX(o.size) as size, o.health as health"). + Select("o.object_id as name, o.size as size, o.health as health"). Model(&dbObject{}). Table("objects o"). Joins("INNER JOIN buckets b ON o.db_bucket_id = b.id"). Where("INSTR(o.object_id, ?) > 0 AND b.name = ?", substring, bucket). - Group("o.object_id"). + Order("o.object_id ASC"). Offset(offset). Limit(limit). Scan(&objects).Error