This commit is contained in:
+256
-15
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"vctp/db/queries"
|
||||
@@ -49,6 +50,21 @@ type loggerContextKey struct{}
|
||||
|
||||
var ensureOnceRegistry sync.Map
|
||||
|
||||
var vmHourlyStatsPostgresPartitioningEnabled atomic.Bool
|
||||
|
||||
func init() {
|
||||
vmHourlyStatsPostgresPartitioningEnabled.Store(false)
|
||||
}
|
||||
|
||||
// SetVmHourlyStatsPostgresPartitioningEnabled toggles postgres monthly partitioning for vm_hourly_stats.
|
||||
func SetVmHourlyStatsPostgresPartitioningEnabled(enabled bool) {
|
||||
vmHourlyStatsPostgresPartitioningEnabled.Store(enabled)
|
||||
}
|
||||
|
||||
func vmHourlyStatsPartitioningEnabled() bool {
|
||||
return vmHourlyStatsPostgresPartitioningEnabled.Load()
|
||||
}
|
||||
|
||||
// WithLoggerContext stores a logger in context for downstream DB helper logging.
|
||||
func WithLoggerContext(ctx context.Context, logger *slog.Logger) context.Context {
|
||||
if ctx == nil {
|
||||
@@ -700,7 +716,18 @@ func CheckpointDatabase(ctx context.Context, dbConn *sqlx.DB) (string, error) {
|
||||
|
||||
// EnsureVmHourlyStats creates the shared per-snapshot cache table used by Go aggregations.
|
||||
func EnsureVmHourlyStats(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
ddl := `
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
if (driver == "pgx" || driver == "postgres") && vmHourlyStatsPartitioningEnabled() {
|
||||
return ensureOncePerDB(dbConn, "vm_hourly_stats_partitioned", func() error {
|
||||
if err := ensureVmHourlyStatsPartitionedPostgres(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureVmHourlyStatsIndexes(ctx, dbConn, driver)
|
||||
})
|
||||
}
|
||||
|
||||
return ensureOncePerDB(dbConn, "vm_hourly_stats_unpartitioned", func() error {
|
||||
ddl := `
|
||||
CREATE TABLE IF NOT EXISTS vm_hourly_stats (
|
||||
"SnapshotTime" BIGINT NOT NULL,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
@@ -721,27 +748,241 @@ CREATE TABLE IF NOT EXISTS vm_hourly_stats (
|
||||
"SrmPlaceholder" TEXT,
|
||||
PRIMARY KEY ("Vcenter","VmId","SnapshotTime")
|
||||
);`
|
||||
return ensureOncePerDB(dbConn, "vm_hourly_stats", func() error {
|
||||
if _, err := execLog(ctx, dbConn, ddl); err != nil {
|
||||
return err
|
||||
}
|
||||
indexQueries := []string{
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vmuuid_time_idx ON vm_hourly_stats ("VmUuid","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vmid_time_idx ON vm_hourly_stats ("VmId","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_name_time_idx ON vm_hourly_stats (lower("Name"),"SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_snapshottime_idx ON vm_hourly_stats ("SnapshotTime")`,
|
||||
return ensureVmHourlyStatsIndexes(ctx, dbConn, driver)
|
||||
})
|
||||
}
|
||||
|
||||
// EnsureVmHourlyStatsPartitionForSnapshot creates the month partition for snapshotUnix when postgres partitioning is enabled.
|
||||
func EnsureVmHourlyStatsPartitionForSnapshot(ctx context.Context, dbConn *sqlx.DB, snapshotUnix int64) error {
|
||||
driver := strings.ToLower(dbConn.DriverName())
|
||||
if driver != "pgx" && driver != "postgres" {
|
||||
return nil
|
||||
}
|
||||
if !vmHourlyStatsPartitioningEnabled() || snapshotUnix <= 0 {
|
||||
return nil
|
||||
}
|
||||
snapshotMonth := time.Unix(snapshotUnix, 0).UTC().Format("200601")
|
||||
key := "vm_hourly_stats_partition_month_" + snapshotMonth
|
||||
return ensureOncePerDB(dbConn, key, func() error {
|
||||
partitioned, err := isVmHourlyStatsPartitioned(ctx, dbConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
failedIndexes := 0
|
||||
for _, q := range indexQueries {
|
||||
if _, err := execLog(ctx, dbConn, q); err != nil {
|
||||
failedIndexes++
|
||||
if !partitioned {
|
||||
return nil
|
||||
}
|
||||
return ensureVmHourlyStatsMonthPartition(ctx, dbConn, time.Unix(snapshotUnix, 0).UTC())
|
||||
})
|
||||
}
|
||||
|
||||
func ensureVmHourlyStatsIndexes(ctx context.Context, dbConn *sqlx.DB, driver string) error {
|
||||
indexQueries := []string{
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_snapshottime_idx ON vm_hourly_stats ("SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vmuuid_time_idx ON vm_hourly_stats ("VmUuid","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vmid_time_idx ON vm_hourly_stats ("VmId","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_name_time_idx ON vm_hourly_stats (lower("Name"),"SnapshotTime")`,
|
||||
}
|
||||
|
||||
if driver == "pgx" || driver == "postgres" {
|
||||
indexQueries = append(indexQueries,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vcenter_time_idx ON vm_hourly_stats ("Vcenter","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vcenter_vmid_time_idx ON vm_hourly_stats ("Vcenter","VmId","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vcenter_vmuuid_time_idx ON vm_hourly_stats ("Vcenter","VmUuid","SnapshotTime")`,
|
||||
`CREATE INDEX IF NOT EXISTS vm_hourly_stats_vcenter_name_time_idx ON vm_hourly_stats ("Vcenter",lower("Name"),"SnapshotTime")`,
|
||||
)
|
||||
}
|
||||
|
||||
failedIndexes := 0
|
||||
for _, q := range indexQueries {
|
||||
if _, err := execLog(ctx, dbConn, q); err != nil {
|
||||
failedIndexes++
|
||||
}
|
||||
}
|
||||
if failedIndexes > 0 {
|
||||
slog.Warn("vm_hourly_stats index ensure incomplete; continuing without retries until restart", "failed_indexes", failedIndexes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureVmHourlyStatsPartitionedPostgres(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
partitioned, err := isVmHourlyStatsPartitioned(ctx, dbConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !partitioned {
|
||||
exists := TableExists(ctx, dbConn, "vm_hourly_stats")
|
||||
if exists {
|
||||
if err := migrateVmHourlyStatsToPartitioned(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := execLog(ctx, dbConn, vmHourlyStatsPartitionedDDL()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if failedIndexes > 0 {
|
||||
slog.Warn("vm_hourly_stats index ensure incomplete; continuing without retries until restart", "failed_indexes", failedIndexes)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureVmHourlyStatsDefaultPartition(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureVmHourlyStatsPartitionsForExistingData(ctx, dbConn); err != nil {
|
||||
return err
|
||||
}
|
||||
nowUTC := time.Now().UTC()
|
||||
if err := ensureVmHourlyStatsMonthPartition(ctx, dbConn, nowUTC); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureVmHourlyStatsMonthPartition(ctx, dbConn, nowUTC.AddDate(0, 1, 0)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func vmHourlyStatsPartitionedDDL() string {
|
||||
return `
|
||||
CREATE TABLE IF NOT EXISTS vm_hourly_stats (
|
||||
"SnapshotTime" BIGINT NOT NULL,
|
||||
"Vcenter" TEXT NOT NULL,
|
||||
"VmId" TEXT,
|
||||
"VmUuid" TEXT,
|
||||
"Name" TEXT,
|
||||
"CreationTime" BIGINT,
|
||||
"DeletionTime" BIGINT,
|
||||
"ResourcePool" TEXT,
|
||||
"Datacenter" TEXT,
|
||||
"Cluster" TEXT,
|
||||
"Folder" TEXT,
|
||||
"ProvisionedDisk" REAL,
|
||||
"VcpuCount" BIGINT,
|
||||
"RamGB" BIGINT,
|
||||
"IsTemplate" TEXT,
|
||||
"PoweredOn" TEXT,
|
||||
"SrmPlaceholder" TEXT,
|
||||
CONSTRAINT vm_hourly_stats_partitioned_pk PRIMARY KEY ("Vcenter","VmId","SnapshotTime")
|
||||
) PARTITION BY RANGE ("SnapshotTime");
|
||||
`
|
||||
}
|
||||
|
||||
func isVmHourlyStatsPartitioned(ctx context.Context, dbConn *sqlx.DB) (bool, error) {
|
||||
var count int
|
||||
err := getLog(ctx, dbConn, &count, `
|
||||
SELECT COUNT(1)
|
||||
FROM pg_partitioned_table pt
|
||||
JOIN pg_class c ON c.oid = pt.partrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND c.relname = 'vm_hourly_stats'
|
||||
`)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func migrateVmHourlyStatsToPartitioned(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
tx, err := dbConn.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `LOCK TABLE vm_hourly_stats IN ACCESS EXCLUSIVE MODE`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var partitionedCount int
|
||||
if err := tx.GetContext(ctx, &partitionedCount, `
|
||||
SELECT COUNT(1)
|
||||
FROM pg_partitioned_table pt
|
||||
JOIN pg_class c ON c.oid = pt.partrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
AND c.relname = 'vm_hourly_stats'
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
if partitionedCount > 0 {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
backupTable := fmt.Sprintf("vm_hourly_stats_unpartitioned_%d", time.Now().UTC().UnixNano())
|
||||
if _, err := SafeTableName(backupTable); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, fmt.Sprintf(`ALTER TABLE vm_hourly_stats RENAME TO %s`, backupTable)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, vmHourlyStatsPartitionedDDL()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS vm_hourly_stats_default PARTITION OF vm_hourly_stats DEFAULT`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, fmt.Sprintf(`INSERT INTO vm_hourly_stats SELECT * FROM %s`, backupTable)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, fmt.Sprintf(`DROP TABLE %s`, backupTable)); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func ensureVmHourlyStatsDefaultPartition(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
_, err := execLog(ctx, dbConn, `CREATE TABLE IF NOT EXISTS vm_hourly_stats_default PARTITION OF vm_hourly_stats DEFAULT`)
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureVmHourlyStatsPartitionsForExistingData(ctx context.Context, dbConn *sqlx.DB) error {
|
||||
var bounds struct {
|
||||
Min sql.NullInt64 `db:"min_snapshot"`
|
||||
Max sql.NullInt64 `db:"max_snapshot"`
|
||||
}
|
||||
if err := getLog(ctx, dbConn, &bounds, `
|
||||
SELECT MIN("SnapshotTime") AS min_snapshot, MAX("SnapshotTime") AS max_snapshot
|
||||
FROM vm_hourly_stats
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
if !bounds.Min.Valid || !bounds.Max.Valid {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
start := monthStartUTC(time.Unix(bounds.Min.Int64, 0).UTC())
|
||||
end := monthStartUTC(time.Unix(bounds.Max.Int64, 0).UTC())
|
||||
guard := 0
|
||||
for m := start; !m.After(end); m = m.AddDate(0, 1, 0) {
|
||||
if err := ensureVmHourlyStatsMonthPartition(ctx, dbConn, m); err != nil {
|
||||
return err
|
||||
}
|
||||
guard++
|
||||
if guard > 240 {
|
||||
return fmt.Errorf("vm_hourly_stats partition range guard exceeded while creating existing-data partitions")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureVmHourlyStatsMonthPartition(ctx context.Context, dbConn *sqlx.DB, month time.Time) error {
|
||||
start := monthStartUTC(month.UTC())
|
||||
end := start.AddDate(0, 1, 0)
|
||||
partitionName := fmt.Sprintf("vm_hourly_stats_%s", start.Format("200601"))
|
||||
if _, err := SafeTableName(partitionName); err != nil {
|
||||
return err
|
||||
}
|
||||
query := fmt.Sprintf(
|
||||
`CREATE TABLE IF NOT EXISTS %s PARTITION OF vm_hourly_stats FOR VALUES FROM (%d) TO (%d)`,
|
||||
partitionName,
|
||||
start.Unix(),
|
||||
end.Unix(),
|
||||
)
|
||||
_, err := execLog(ctx, dbConn, query)
|
||||
return err
|
||||
}
|
||||
|
||||
func monthStartUTC(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
// EnsureVmLifecycleCache creates an upsert cache for first/last seen VM info.
|
||||
|
||||
Reference in New Issue
Block a user