From 448fd4f2c1f851799f83e24fa330db4aab329aa7 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 27 Feb 2024 18:31:47 +0100 Subject: [PATCH 1/5] stores: update findAggregatedContractPeriods to fetch individual contracts --- stores/metrics.go | 64 ++++++++++----------- stores/migrations/mysql/metrics/schema.sql | 1 + stores/migrations/sqlite/metrics/schema.sql | 1 + 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/stores/metrics.go b/stores/metrics.go index 8816d1729..7b59a40a5 100644 --- a/stores/metrics.go +++ b/stores/metrics.go @@ -522,43 +522,43 @@ func (s *SQLStore) findAggregatedContractPeriods(start time.Time, n uint64, inte return nil, api.ErrMaxIntervalsExceeded } end := start.Add(time.Duration(n) * interval) - var metricsWithPeriod []struct { + + type metricWithPeriod struct { Metric dbContractMetric `gorm:"embedded"` Period int64 } - err := s.dbMetrics.Raw(` - WITH RECURSIVE periods AS ( - SELECT ? AS period_start - UNION ALL - SELECT period_start + ? - FROM periods - WHERE period_start < ? - ? - ) - SELECT contracts.*, i.Period FROM contracts - INNER JOIN ( - SELECT - p.period_start as Period, - MIN(c.id) AS id - FROM - periods p - INNER JOIN - contracts c ON c.timestamp >= p.period_start AND c.timestamp < p.period_start + ? - GROUP BY - p.period_start, c.fcid - ORDER BY - p.period_start ASC - ) i ON contracts.id = i.id - `, unixTimeMS(start), - interval.Milliseconds(), - unixTimeMS(end), - interval.Milliseconds(), - interval.Milliseconds(), - ). - Scan(&metricsWithPeriod). - Error + var metricsWithPeriod []metricWithPeriod + + err := s.dbMetrics.Transaction(func(tx *gorm.DB) error { + var fcids []fileContractID + if err := tx.Raw("SELECT DISTINCT fcid FROM contracts WHERE contracts.timestamp >= ? AND contracts.timestamp < ?", unixTimeMS(start), unixTimeMS(end)). + Scan(&fcids).Error; err != nil { + return fmt.Errorf("failed to fetch distinct contract ids: %w", err) + } + + for intervalStart := start; intervalStart.Before(end); intervalStart = intervalStart.Add(interval) { + intervalEnd := intervalStart.Add(interval) + for _, fcid := range fcids { + var metrics []dbContractMetric + err := tx.Raw("SELECT * FROM contracts WHERE contracts.timestamp >= ? AND contracts.timestamp < ? AND contracts.fcid = ? LIMIT 1", unixTimeMS(intervalStart), unixTimeMS(intervalEnd), fileContractID(fcid)). + Scan(&metrics).Error + if err != nil { + return fmt.Errorf("failed to fetch contract metrics: %w", err) + } else if len(metrics) == 0 { + continue + } + metricsWithPeriod = append(metricsWithPeriod, metricWithPeriod{ + Metric: metrics[0], + Period: intervalStart.UnixMilli(), + }) + } + } + return nil + }) if err != nil { - return nil, fmt.Errorf("failed to fetch aggregate metrics: %w", err) + return nil, err } + currentPeriod := int64(math.MinInt64) var metrics []dbContractMetric for _, m := range metricsWithPeriod { diff --git a/stores/migrations/mysql/metrics/schema.sql b/stores/migrations/mysql/metrics/schema.sql index 6d993f0cb..4b36e6b99 100644 --- a/stores/migrations/mysql/metrics/schema.sql +++ b/stores/migrations/mysql/metrics/schema.sql @@ -83,6 +83,7 @@ CREATE TABLE `contracts` ( KEY `idx_remaining_funds` (`remaining_funds_lo`,`remaining_funds_hi`), KEY `idx_delete_spending` (`delete_spending_lo`,`delete_spending_hi`), KEY `idx_list_spending` (`list_spending_lo`,`list_spending_hi`) + KEY `idx_contracts_fcid_timestamp` (`fcid`,`timestamp`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- dbPerformanceMetric diff --git a/stores/migrations/sqlite/metrics/schema.sql b/stores/migrations/sqlite/metrics/schema.sql index 4aa174209..63dae7d65 100644 --- a/stores/migrations/sqlite/metrics/schema.sql +++ b/stores/migrations/sqlite/metrics/schema.sql @@ -11,6 +11,7 @@ CREATE INDEX `idx_download_spending` ON `contracts`(`download_spending_lo`,`down CREATE INDEX `idx_upload_spending` ON `contracts`(`upload_spending_lo`,`upload_spending_hi`); CREATE INDEX `idx_contracts_revision_number` ON `contracts`(`revision_number`); CREATE INDEX `idx_remaining_funds` ON `contracts`(`remaining_funds_lo`,`remaining_funds_hi`); +CREATE INDEX `idx_contracts_fcid_timestamp` ON `contracts`(`fcid`,`timestamp`); -- dbContractPruneMetric CREATE TABLE `contract_prunes` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at` datetime,`timestamp` BIGINT NOT NULL,`fcid` blob NOT NULL,`host` blob NOT NULL,`host_version` text,`pruned` BIGINT NOT NULL,`remaining` BIGINT NOT NULL,`duration` integer NOT NULL); From a32cd3befbcdac2dbcfac1007ea112cf6c7ea6c6 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 28 Feb 2024 08:54:39 +0100 Subject: [PATCH 2/5] stores: add migrations --- .../migration_00001_idx_contracts_fcid_timestamp.sql | 1 + .../migration_00001_idx_contracts_fcid_timestamp.sql | 1 + stores/migrations_metrics.go | 12 +++++++++--- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 stores/migrations/mysql/metrics/migration_00001_idx_contracts_fcid_timestamp.sql create mode 100644 stores/migrations/sqlite/metrics/migration_00001_idx_contracts_fcid_timestamp.sql diff --git a/stores/migrations/mysql/metrics/migration_00001_idx_contracts_fcid_timestamp.sql b/stores/migrations/mysql/metrics/migration_00001_idx_contracts_fcid_timestamp.sql new file mode 100644 index 000000000..5276a3083 --- /dev/null +++ b/stores/migrations/mysql/metrics/migration_00001_idx_contracts_fcid_timestamp.sql @@ -0,0 +1 @@ +CREATE INDEX `idx_contracts_fcid_timestamp` ON `contracts`(`fcid`,`timestamp`); diff --git a/stores/migrations/sqlite/metrics/migration_00001_idx_contracts_fcid_timestamp.sql b/stores/migrations/sqlite/metrics/migration_00001_idx_contracts_fcid_timestamp.sql new file mode 100644 index 000000000..5276a3083 --- /dev/null +++ b/stores/migrations/sqlite/metrics/migration_00001_idx_contracts_fcid_timestamp.sql @@ -0,0 +1 @@ +CREATE INDEX `idx_contracts_fcid_timestamp` ON `contracts`(`fcid`,`timestamp`); diff --git a/stores/migrations_metrics.go b/stores/migrations_metrics.go index 60c62c476..fc3164bee 100644 --- a/stores/migrations_metrics.go +++ b/stores/migrations_metrics.go @@ -8,20 +8,26 @@ import ( "gorm.io/gorm" ) -func performMetricsMigrations(tx *gorm.DB, logger *zap.SugaredLogger) error { +func performMetricsMigrations(db *gorm.DB, logger *zap.SugaredLogger) error { dbIdentifier := "metrics" migrations := []*gormigrate.Migration{ { ID: "00001_init", Migrate: func(tx *gorm.DB) error { return errRunV072 }, }, + { + ID: "00001_idx_contracts_fcid_timestamp", + Migrate: func(tx *gorm.DB) error { + return performMigration(tx, dbIdentifier, "00001_idx_contracts_fcid_timestamp", logger) + }, + }, } // Create migrator. - m := gormigrate.New(tx, gormigrate.DefaultOptions, migrations) + m := gormigrate.New(db, gormigrate.DefaultOptions, migrations) // Set init function. - m.InitSchema(initSchema(tx, dbIdentifier, logger)) + m.InitSchema(initSchema(db, dbIdentifier, logger)) // Perform migrations. if err := m.Migrate(); err != nil { From 82f1f5d153815f7a855b8e6375aff19b714dd18c Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 28 Feb 2024 09:25:00 +0100 Subject: [PATCH 3/5] stores: add contractMetricGranularity --- stores/metrics.go | 19 +++++++++++++++++++ stores/metrics_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/stores/metrics.go b/stores/metrics.go index 7b59a40a5..4137f3f65 100644 --- a/stores/metrics.go +++ b/stores/metrics.go @@ -14,6 +14,10 @@ import ( "gorm.io/gorm/clause" ) +const ( + contractMetricGranularity = 5 * time.Minute +) + type ( // dbContractMetric tracks information about a contract's funds. It is // supposed to be reported by a worker every time a contract is revised. @@ -246,6 +250,21 @@ func (s *SQLStore) RecordContractMetric(ctx context.Context, metrics ...api.Cont } } return s.dbMetrics.Transaction(func(tx *gorm.DB) error { + // delete any existing metric for the same contract that has happened + // within the same 30 seconds window by diving the timestamp by 30 seconds and use integer division. + for _, metric := range metrics { + intervalStart := metric.Timestamp.Std().Truncate(contractMetricGranularity) + intervalEnd := intervalStart.Add(contractMetricGranularity) + err := tx. + Where("timestamp >= ?", unixTimeMS(intervalStart)). + Where("timestamp < ?", unixTimeMS(intervalEnd)). + Where("fcid", fileContractID(metric.ContractID)). + Delete(&dbContractMetric{}). + Error + if err != nil { + return err + } + } return tx.Create(&dbMetrics).Error }) } diff --git a/stores/metrics_test.go b/stores/metrics_test.go index f71d985bd..ec97099ba 100644 --- a/stores/metrics_test.go +++ b/stores/metrics_test.go @@ -488,6 +488,30 @@ func TestContractMetrics(t *testing.T) { } else if len(metrics) != 1 { t.Fatalf("expected 1 metric, got %v", len(metrics)) } + + // Drop all metrics. + if err := ss.dbMetrics.Where("TRUE").Delete(&dbContractMetric{}).Error; err != nil { + t.Fatal(err) + } + + // Record multiple metrics for the same contract - one per second over 10 minutes + for i := int64(0); i < 600; i++ { + err := ss.RecordContractMetric(context.Background(), api.ContractMetric{ + ContractID: types.FileContractID{1}, + Timestamp: api.TimeRFC3339(time.Unix(i, 0)), + }) + if err != nil { + t.Fatal(err) + } + } + + // Check how many metrics were recorded. + var n int64 + if err := ss.dbMetrics.Model(&dbContractMetric{}).Count(&n).Error; err != nil { + t.Fatal(err) + } else if n != 2 { + t.Fatalf("expected 2 metrics, got %v", n) + } } func TestWalletMetrics(t *testing.T) { From 7db0f926ee3f30bab346a37e95bc14193ac0d618 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Wed, 28 Feb 2024 10:03:07 +0100 Subject: [PATCH 4/5] stores: fix syntax error --- stores/migrations/mysql/metrics/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/migrations/mysql/metrics/schema.sql b/stores/migrations/mysql/metrics/schema.sql index 4b36e6b99..da4db5a6e 100644 --- a/stores/migrations/mysql/metrics/schema.sql +++ b/stores/migrations/mysql/metrics/schema.sql @@ -82,7 +82,7 @@ CREATE TABLE `contracts` ( KEY `idx_contracts_timestamp` (`timestamp`), KEY `idx_remaining_funds` (`remaining_funds_lo`,`remaining_funds_hi`), KEY `idx_delete_spending` (`delete_spending_lo`,`delete_spending_hi`), - KEY `idx_list_spending` (`list_spending_lo`,`list_spending_hi`) + KEY `idx_list_spending` (`list_spending_lo`,`list_spending_hi`), KEY `idx_contracts_fcid_timestamp` (`fcid`,`timestamp`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; From 75afd5624357b09ea1b685353b0686354fed16ce Mon Sep 17 00:00:00 2001 From: Christopher Schinnerl Date: Thu, 29 Feb 2024 09:16:15 +0100 Subject: [PATCH 5/5] Update stores/metrics.go Co-authored-by: Peter-Jan Brone --- stores/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/metrics.go b/stores/metrics.go index 4137f3f65..333ed8a42 100644 --- a/stores/metrics.go +++ b/stores/metrics.go @@ -251,7 +251,7 @@ func (s *SQLStore) RecordContractMetric(ctx context.Context, metrics ...api.Cont } return s.dbMetrics.Transaction(func(tx *gorm.DB) error { // delete any existing metric for the same contract that has happened - // within the same 30 seconds window by diving the timestamp by 30 seconds and use integer division. + // within the same 5' window by diving the timestamp by 5' and use integer division. for _, metric := range metrics { intervalStart := metric.Timestamp.Std().Truncate(contractMetricGranularity) intervalEnd := intervalStart.Add(contractMetricGranularity)