diff --git a/internal/tasks/inventoryHelpers.go b/internal/tasks/inventoryHelpers.go index 31259ad..e40cd62 100644 --- a/internal/tasks/inventoryHelpers.go +++ b/internal/tasks/inventoryHelpers.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strconv" "strings" "time" @@ -43,18 +44,47 @@ func boolStringFromInterface(value interface{}) string { // latestHourlySnapshotBefore finds the most recent hourly snapshot table prior to the given time, skipping empty tables. func latestHourlySnapshotBefore(ctx context.Context, dbConn *sqlx.DB, cutoff time.Time) (string, error) { - rows, err := dbConn.QueryxContext(ctx, ` -SELECT table_name, snapshot_time, snapshot_count -FROM snapshot_registry -WHERE snapshot_type = 'hourly' AND snapshot_time < ? AND snapshot_count > 0 -ORDER BY snapshot_time DESC -LIMIT 50 -`, cutoff.Unix()) + tables, err := listLatestHourlyWithRows(ctx, dbConn, "", cutoff.Unix(), 1) if err != nil { return "", err } + if len(tables) == 0 { + return "", nil + } + return tables[0].Table, nil +} + +// parseSnapshotTime extracts the unix suffix from an inventory_hourly table name. +func parseSnapshotTime(table string) (int64, bool) { + const prefix = "inventory_hourly_" + if !strings.HasPrefix(table, prefix) { + return 0, false + } + ts, err := strconv.ParseInt(strings.TrimPrefix(table, prefix), 10, 64) + if err != nil { + return 0, false + } + return ts, true +} + +// listLatestHourlyWithRows returns recent hourly snapshot tables (ordered desc by time) that have rows, optionally filtered by vcenter. +func listLatestHourlyWithRows(ctx context.Context, dbConn *sqlx.DB, vcenter string, beforeUnix int64, limit int) ([]snapshotTable, error) { + if limit <= 0 { + limit = 50 + } + rows, err := dbConn.QueryxContext(ctx, ` +SELECT table_name, snapshot_time, snapshot_count +FROM snapshot_registry +WHERE snapshot_type = 'hourly' AND snapshot_time < ? +ORDER BY snapshot_time DESC +LIMIT ? +`, beforeUnix, limit) + if err != nil { + return nil, err + } defer rows.Close() + var out []snapshotTable for rows.Next() { var name string var ts int64 @@ -65,23 +95,34 @@ LIMIT 50 if err := db.ValidateTableName(name); err != nil { continue } - // Rely on snapshot_count to avoid costly table scans; fall back to a cheap row check only if count is zero. - if count > 0 { - return name, nil + // Use snapshot_count first; fall back to row check (and vcenter filter) only when needed. + if count == 0 { + if has, _ := db.TableHasRows(ctx, dbConn, name); !has { + continue + } } - if hasRows, _ := db.TableHasRows(ctx, dbConn, name); hasRows { - return name, nil + if vcenter != "" { + vrows, qerr := querySnapshotRows(ctx, dbConn, name, []string{"VmId"}, `"Vcenter" = ? LIMIT 1`, vcenter) + if qerr != nil { + continue + } + hasVcenter := vrows.Next() + vrows.Close() + if !hasVcenter { + continue + } } + out = append(out, snapshotTable{Table: name, Time: ts}) } - return "", nil + return out, nil } -// HasSnapshotGap reports whether the gap between prev and curr exceeds 2x the expected interval. -func HasSnapshotGap(prevUnix, currUnix int64, expectedSeconds int64) bool { +// SnapshotTooSoon reports whether the gap between prev and curr is significantly shorter than expected (default: <50% interval). +func SnapshotTooSoon(prevUnix, currUnix int64, expectedSeconds int64) bool { if prevUnix == 0 || currUnix == 0 || expectedSeconds <= 0 { return false } - return currUnix-prevUnix > expectedSeconds*2 + return currUnix-prevUnix < expectedSeconds/2 } // querySnapshotRows builds a SELECT with proper rebind for the given table/columns/where. diff --git a/internal/tasks/inventorySnapshots.go b/internal/tasks/inventorySnapshots.go index 434be75..ed5fd80 100644 --- a/internal/tasks/inventorySnapshots.go +++ b/internal/tasks/inventorySnapshots.go @@ -1104,23 +1104,16 @@ func (c *CronTask) compareWithPreviousSnapshot( c.Logger.Warn("failed to locate previous hourly snapshot for deletion comparison", "error", prevTableErr, "url", url) } - prevSnapshotTime := int64(0) - if prevTableName != "" { - if suffix := strings.TrimPrefix(prevTableName, "inventory_hourly_"); suffix != prevTableName { - if ts, err := strconv.ParseInt(suffix, 10, 64); err == nil { - prevSnapshotTime = ts - } - } - } + prevSnapshotTime, _ := parseSnapshotTime(prevTableName) newCount := 0 if prevTableName != "" { moreMissing := c.markMissingFromPrevious(ctx, dbConn, prevTableName, url, startTime, presentSnapshots, presentByUuid, presentByName, inventoryByVmID, inventoryByUuid, inventoryByName) missingCount += moreMissing expectedSeconds := int64(durationFromSeconds(c.Settings.Values.Settings.VcenterInventorySnapshotSeconds, time.Hour).Seconds()) - // Allow runs as soon as half the normal interval; treat larger gaps as unreliable for "new" detection. - if HasSnapshotGap(prevSnapshotTime, startTime.Unix(), expectedSeconds/2) { - c.Logger.Info("skipping new-VM detection due to gap between snapshots", "prev_table", prevTableName, "prev_snapshot_unix", prevSnapshotTime, "current_snapshot_unix", startTime.Unix()) + // Skip only if snapshots are much closer together than the configured cadence (e.g., rerun inside half interval). + if SnapshotTooSoon(prevSnapshotTime, startTime.Unix(), expectedSeconds) { + c.Logger.Info("skipping new-VM detection because snapshots are too close together", "prev_table", prevTableName, "prev_snapshot_unix", prevSnapshotTime, "current_snapshot_unix", startTime.Unix(), "expected_interval_seconds", expectedSeconds) } else { newCount = countNewFromPrevious(ctx, dbConn, prevTableName, url, presentSnapshots) if newCount > 0 {