Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/34677-software-versions-endpoint
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved performance of `/api/latest/fleet/software/versions` API endpoint.
13 changes: 12 additions & 1 deletion frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTitles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ const SoftwareTitles = ({
}
);

// This query checks if there are any installable software titles (VPP apps or Fleet-managed
// installers) available for the team. It only runs when the versions table is empty, to
// determine which empty state message to show:
// - If installable software exists: "Install software on your hosts to see versions."
// - If no installable software: "Expecting to see software? Check back later."
// See PR #21118 (issue #21053) for context.
//
// The enabled condition ensures this query only fires after the versions query has fully loaded
// and confirmed it's actually empty, preventing unnecessary API call delay during page transitions.
const {
data: titlesAvailableForInstallResponse,
isFetching: isTitlesAFIFetching,
Expand Down Expand Up @@ -164,7 +173,9 @@ const SoftwareTitles = ({
...QUERY_OPTIONS,
enabled:
location.pathname === PATHS.SOFTWARE_VERSIONS &&
versionsData &&
!isVersionsLoading &&
!isVersionsFetching &&
versionsData !== undefined &&
versionsData.count === 0,
}
);
Expand Down
10 changes: 8 additions & 2 deletions server/datastore/mysql/common_mysql/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ func WithRetryTxx(ctx context.Context, db *sqlx.DB, fn TxFn, logger log.Logger)
return backoff.Retry(operation, bo)
}

// retryableError determines whether a MySQL error can be retried. By default
// RetryableError determines whether a MySQL error can be retried. By default
// errors are considered non-retryable. Only errors that we know have a
// possibility of succeeding on a retry should return true in this function.
func retryableError(err error) bool {
func RetryableError(err error) bool {
base := ctxerr.Cause(err)
if b, ok := base.(*mysql.MySQLError); ok {
switch b.Number {
Expand All @@ -95,3 +95,9 @@ func retryableError(err error) bool {

return false
}

// retryableError is the internal (non-exported) version that calls RetryableError.
// Kept for backwards compatibility with existing callers in this package.
func retryableError(err error) bool {
return RetryableError(err)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20251117020000, Down_20251117020000)
}

func Up_20251117020000(tx *sql.Tx) error {
// Drop the old index that doesn't include global_stats
_, err := tx.Exec(`
DROP INDEX idx_software_host_counts_team_id_hosts_count_software_id
ON software_host_counts
`)
if err != nil {
return fmt.Errorf("failed to drop old index: %w", err)
}

// Create new optimized index with global_stats and DESC on hosts_count
// This allows MySQL to:
// 1. Seek directly to team_id + global_stats combination
// 2. Read rows in descending hosts_count order (already sorted)
// 3. Stop after finding LIMIT rows
_, err = tx.Exec(`
CREATE INDEX idx_software_host_counts_team_global_hosts_desc
ON software_host_counts (team_id, global_stats, hosts_count DESC, software_id)
`)
if err != nil {
return fmt.Errorf("failed to create new index: %w", err)
}

return nil
}

func Down_20251117020000(_ *sql.Tx) error {
return nil
}
6 changes: 3 additions & 3 deletions server/datastore/mysql/schema.sql

Large diffs are not rendered by default.

Loading
Loading