Fixed MySQL DB performance regressions - #33184
Conversation
…3.1 affecting OS versions and software titles read queries.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #33184 +/- ##
==========================================
+ Coverage 63.94% 63.96% +0.02%
==========================================
Files 2048 2050 +2
Lines 202292 202618 +326
Branches 6664 6664
==========================================
+ Hits 129351 129602 +251
- Misses 62752 62817 +65
- Partials 10189 10199 +10
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
WalkthroughAdds a batch vulnerabilities lookup across multiple OS versions, updates the OS versions service to use it, optimizes software title lookups via composite IN, and introduces a MySQL index and migration to speed kernel host count queries. Updates datastore interface and mocks, adjusts tests, and updates schema metadata. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client
participant API as Service: OSVersions
participant DS as Datastore
participant MySQL as MySQL
Client->>API: GET /api/latest/fleet/os_versions?team_id&include_cvss
API->>DS: ListVulnsByMultipleOSVersions(osVersions[], includeCVSS, teamID)
DS->>MySQL: Resolve OS IDs for (name,version) batch
MySQL-->>DS: OS IDs map
DS->>MySQL: Query OS vulns for all OS IDs
MySQL-->>DS: Rows (CVE, created_at, resolved_in, ...)
DS->>MySQL: Query kernel vulns filtered by linux OS IDs (+team filter)
MySQL-->>DS: Kernel rows
alt includeCVSS
DS->>MySQL: Batch fetch CVE metadata
MySQL-->>DS: CVSS/EPSS/CISA/published/description
DS->>DS: Apply metadata per CVE
end
DS-->>API: map[name-version] -> Vulnerabilities
API->>API: Populate OSVersion entries (vulns, CPEs)
API-->>Client: JSON: OS versions with vulnerabilities
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/datastore/mysql/software.go (1)
659-666: Compile-time bug: undefined variable at return + inadvertent map reset.
- existingBundleIDsToUpdate is defined inside the if-block; it’s out of scope at the return on Line 684.
- incomingChecksumToTitle is reinitialized on Line 663, wiping entries populated for non-bundle titles.
Fix by hoisting existingBundleIDsToUpdate to function scope and removing the unintended reinit:
@@ func (ds *Datastore) getIncomingSoftwareChecksumsToExistingTitles( @@ ) (map[string]fleet.SoftwareTitle, map[string]fleet.Software, error) { var ( incomingChecksumToTitle = make(map[string]fleet.SoftwareTitle, len(newSoftwareChecksums)) argsWithoutBundleIdentifier []any argsWithBundleIdentifier []any uniqueTitleStrToChecksum = make(map[string]string) ) + // keep second return value stable and accessible to the whole function + existingBundleIDsToUpdate := make(map[string]fleet.Software) @@ if len(argsWithBundleIdentifier) > 0 { - // no-op code change - incomingChecksumToTitle = make(map[string]fleet.SoftwareTitle, len(newSoftwareChecksums)) stmtBundleIdentifier := `SELECT id, name, source, browser, bundle_identifier FROM software_titles WHERE bundle_identifier IN (?)`
🧹 Nitpick comments (16)
server/datastore/mysql/software.go (2)
630-637: Guard against placeholder/arg mismatch for composite IN.Add a sanity check to avoid hard-to-debug SQL errors if the args slice ever gets out of sync.
@@ - numItems := len(argsWithoutBundleIdentifier) / 3 + if len(argsWithoutBundleIdentifier)%3 != 0 { + return nil, nil, ctxerr.New(ctx, "internal: expected (name,source,browser) triplets") + } + numItems := len(argsWithoutBundleIdentifier) / 3 valuePlaceholders := make([]string, 0, numItems)
613-626: Optional: de-duplicate identical (name, source, browser) triplets before building IN.If multiple new software entries share the same title triplet, you can shrink the IN list and reduce planning time.
I can provide a small set-based prepass if you want it.
changes/33147-increased-db-load (1)
1-2: Enrich the change note with user impact and upgrade guidanceAdd specifics to help operators scanning release notes:
- Name affected endpoints (/api/latest/fleet/os_versions, /api/fleet/orbit/software_install/result, device software install APIs).
- Mention the added MySQL index and batch OS vuln lookup, and that a DB migration runs on upgrade.
- State affected versions (regression in 4.73.0–4.73.1; fixed in this PR’s version).
Apply:
-Fixed MySQL DB performance regressions introduced in Fleet 4.73.0/4.73.1 affecting OS versions and software titles read queries. +Fix MySQL performance regressions introduced in Fleet 4.73.0–4.73.1 that increased load and caused timeouts on large deployments. + +Details: +- Optimizes /api/latest/fleet/os_versions and software install result endpoints by batching OS vulnerability lookups and improving title lookups. +- Adds MySQL index on kernel_host_counts to speed team‑scoped OS version aggregations. +- Includes a DB migration; run `fleet prepare db` during upgrade.server/fleet/datastore.go (2)
1119-1121: Document the key format and input semantics of the new batch API (and consider a helper).The return type is map[string]Vulnerabilities, but the key format isn’t specified. Please document exactly how keys are constructed (e.g., "platform|name|version"), nil vs. empty input behavior, deduplication of duplicate OS versions, and the effect of includeCVSS and teamID. Optionally, export a helper like fleet.MakeOSVersionKey(OSVersion) to avoid caller-side drift.
Apply this comment update:
- // ListVulnsByMultipleOSVersions is an optimized batch query that fetches vulnerabilities for multiple OS versions - // in a single efficient operation. + // ListVulnsByMultipleOSVersions fetches vulnerabilities for multiple OS versions in one call. + // Contract: + // - osVersions may be nil/empty: returns an empty map and no error. + // - Returned map is keyed by a canonical OS version key (e.g., "platform|name|version"). + // - Duplicate inputs are deduplicated by that key. + // - includeCVSS toggles inclusion of CVSS metadata (may be more expensive). + // - teamID (if non-nil) scopes team-specific data (e.g., kernel vulns/counts).
1119-1121: Future-proof the API by preferring an ID-based key (optional).If feasible, prefer map[uint]Vulnerabilities keyed by OS version ID to avoid string canonicalization and reduce ambiguity. If IDs aren’t always available at call sites, keep the string key but add the documented helper to standardize key construction.
server/service/hosts_test.go (3)
1301-1304: Return an empty map instead of nil from the mock for clarity.Explicit empty map avoids surprises if callers iterate or range over results.
- teamID *uint) (map[string]fleet.Vulnerabilities, error) { - return nil, nil + teamID *uint) (map[string]fleet.Vulnerabilities, error) { + return map[string]fleet.Vulnerabilities{}, nil
1346-1349: Same here: prefer an empty map over nil in the mock.- teamID *uint) (map[string]fleet.Vulnerabilities, error) { - return nil, nil + teamID *uint) (map[string]fleet.Vulnerabilities, error) { + return map[string]fleet.Vulnerabilities{}, nil
1301-1304: Optionally assert the batch API is invoked at least once.Add a require.True(t, ds.ListVulnsByMultipleOSVersionsFuncInvoked) in one of these tests (after svc.OSVersions) to ensure the new path is wired.
Also applies to: 1346-1349
server/service/hosts.go (1)
2185-2190: Consider extracting CPE generation to a shared functionThe Darwin CPE generation logic appears in both the batch population (lines 2185-2190) and the single OS version details (line 2326-2331). Consider extracting this to avoid duplication.
+func generateDarwinCPEs(version string) []string { + return []string{ + fmt.Sprintf("cpe:2.3:o:apple:macos:%s:*:*:*:*:*:*:*", version), + fmt.Sprintf("cpe:2.3:o:apple:mac_os_x:%s:*:*:*:*:*:*:*", version), + } +} // In the batch population section: if osV.Platform == "darwin" { - osV.GeneratedCPEs = []string{ - fmt.Sprintf("cpe:2.3:o:apple:macos:%s:*:*:*:*:*:*:*", osV.Version), - fmt.Sprintf("cpe:2.3:o:apple:mac_os_x:%s:*:*:*:*:*:*:*", osV.Version), - } + osV.GeneratedCPEs = generateDarwinCPEs(osV.Version) }server/datastore/mysql/operating_system_vulnerabilities.go (4)
335-337: Tone down perf claim in comment."700x+ improvement" can rot or be environment-specific. Prefer "significant improvement" or reference a benchmark doc.
447-462: Dedup is O(n^2) per OS key; switch to an index map.On large OS cohorts this loop becomes quadratic. Use an index map to track positions by CVE and update CreatedAt in O(1).
Apply this diff (plus a small addition above):
@@ - // Step 2: Execute queries - vulnsByKey := make(map[string][]fleet.CVE) + // Step 2: Execute queries + vulnsByKey := make(map[string][]fleet.CVE) + idxByKey := make(map[string]map[string]int) // key -> CVE -> index @@ - // Check if we already have this CVE for this key (deduplication across architectures) - found := false - for i, existing := range vulnsByKey[key] { - if existing.CVE == r.CVE { - found = true - // Keep the earliest CreatedAt time - if r.CreatedAt.Before(existing.CreatedAt) { - vulnsByKey[key][i].CreatedAt = r.CreatedAt - } - break - } - } - if !found { - vulnsByKey[key] = append(vulnsByKey[key], vuln) - } + if idxByKey[key] == nil { + idxByKey[key] = make(map[string]int) + } + if i, ok := idxByKey[key][r.CVE]; ok { + if r.CreatedAt.Before(vulnsByKey[key][i].CreatedAt) { + vulnsByKey[key][i].CreatedAt = r.CreatedAt + } + } else { + idxByKey[key][r.CVE] = len(vulnsByKey[key]) + vulnsByKey[key] = append(vulnsByKey[key], vuln) + }Do the same pattern in the kernel loop if you expect duplicates there as well.
596-617: Safer double-pointer population for CVE metadata.Using &meta.Field takes the address of a local copy; it escapes and is technically safe but non-idiomatic and easy to misuse. Copy to a local var first for clarity.
Apply this style (repeat for other fields):
- if meta.CVSSScore != nil { - vulns[i].CVSSScore = &meta.CVSSScore - } + if meta.CVSSScore != nil { + cvss := meta.CVSSScore + vulns[i].CVSSScore = &cvss + }
530-534: Comment mismatch.This block applies CVE metadata to all CVEs, not "Linux kernels only." Update the comment.
server/datastore/mysql/operating_system_vulnerabilities_batch_test.go (3)
74-112: Good CVSS/EPSS assertions; consider asserting nil for non-enriched CVEs.Add one negative assertion to ensure only targeted CVE got metadata when includeCVSS=true.
114-134: Team filter test could add a negative case.Also call without teamID (or with a different team) and assert kernel CVE is absent to prove the filter works.
223-275: Helper seeds realistic data; minor nit on rune math.string(rune('0'+i)) works; fmt.Sprintf("%d", i) is clearer but optional.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
changes/33147-increased-db-load(1 hunks)server/datastore/mysql/migrations/tables/20250918154557_AddKernelHostCountsIndexForVulnQueries.go(1 hunks)server/datastore/mysql/operating_system_vulnerabilities.go(2 hunks)server/datastore/mysql/operating_system_vulnerabilities_batch_test.go(1 hunks)server/datastore/mysql/schema.sql(2 hunks)server/datastore/mysql/software.go(1 hunks)server/fleet/datastore.go(1 hunks)server/mock/datastore_mock.go(3 hunks)server/service/hosts.go(1 hunks)server/service/hosts_test.go(2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.go
⚙️ CodeRabbit configuration file
When reviewing SQL queries that are added or modified, ensure that appropriate filtering criteria are applied—especially when a query is intended to return data for a specific entity (e.g., a single host). Check for missing WHERE clauses or incorrect filtering that could lead to incorrect or non-deterministic results (e.g., returning the first row instead of the correct one). Flag any queries that may return unintended results due to lack of precise scoping.
Files:
server/fleet/datastore.goserver/datastore/mysql/software.goserver/service/hosts_test.goserver/service/hosts.goserver/datastore/mysql/operating_system_vulnerabilities.goserver/mock/datastore_mock.goserver/datastore/mysql/migrations/tables/20250918154557_AddKernelHostCountsIndexForVulnQueries.goserver/datastore/mysql/operating_system_vulnerabilities_batch_test.go
🧠 Learnings (3)
📚 Learning: 2025-08-08T07:40:05.301Z
Learnt from: getvictor
PR: fleetdm/fleet#31726
File: server/datastore/mysql/labels_test.go:2031-2031
Timestamp: 2025-08-08T07:40:05.301Z
Learning: In fleetdm/fleet repository tests (server/datastore/mysql/labels_test.go and similar), using testing.T.Context() is valid because the project targets a recent Go version where testing.T.Context() exists. Do not suggest replacing t.Context() with context.Background() in this codebase.
Applied to files:
server/datastore/mysql/operating_system_vulnerabilities.go
📚 Learning: 2025-08-08T07:40:05.301Z
Learnt from: getvictor
PR: fleetdm/fleet#31726
File: server/datastore/mysql/labels_test.go:2031-2031
Timestamp: 2025-08-08T07:40:05.301Z
Learning: Fleet repo targets Go 1.24.5 (root go.mod), which supports testing.T.Context(). Do not flag usage of t.Context() or suggest replacing it with context.Background() in tests (e.g., server/datastore/mysql/labels_test.go Line 2031 and similar).
Applied to files:
server/datastore/mysql/operating_system_vulnerabilities.go
📚 Learning: 2025-08-01T15:08:16.858Z
Learnt from: sgress454
PR: fleetdm/fleet#31508
File: server/datastore/mysql/schema.sql:102-116
Timestamp: 2025-08-01T15:08:16.858Z
Learning: The schema.sql file in server/datastore/mysql/ is auto-generated from migrations for use with tests, so it cannot be manually edited. Any changes must be made through migrations.
Applied to files:
server/datastore/mysql/schema.sql
🔇 Additional comments (14)
server/datastore/mysql/migrations/tables/20250918154557_AddKernelHostCountsIndexForVulnQueries.go (1)
29-31: Provide a reversible Down migration.Drop the index on rollback.
-func Down_20250918154557(_ *sql.Tx) error { - return nil -} +func Down_20250918154557(tx *sql.Tx) error { + if _, err := tx.Exec(`DROP INDEX idx_kernel_host_counts_os_version_software ON kernel_host_counts`); err != nil { + // tolerate absence to keep Down idempotent + _ = err + } + return nil +}⛔ Skipped due to learnings
Learnt from: getvictor PR: fleetdm/fleet#30589 File: server/datastore/mysql/migrations/tables/20250707095725_HostIdentitySCEPCertificates.go:53-55 Timestamp: 2025-07-08T16:13:39.114Z Learning: In the Fleet codebase, Down migration functions are intentionally left empty/no-op. The team does not implement rollback functionality for database migrations, so empty Down_* functions in migration files are correct and should not be flagged as issues.server/datastore/mysql/schema.sql (2)
1501-1503: schema.sql is generated; confirm no manual edits and that dump settings are stableTABLESPACE clause and AUTO_INCREMENT bump look like dump‑format churn from the new migration. Per our learnings, schema.sql must be auto‑generated from migrations; ensure these diffs came from the generator, not hand edits, and that the dump tooling/version remains stable to avoid noisy diffs.
1084-1086: Add a team‑prefixed covering index on kernel_host_countsFound: operating_system_vulnerabilities.go conditionally filters on
kernel_host_counts.team_id(multiple places) while the migration adds onlyidx_kernel_host_counts_os_version_software (os_version_id, software_id, hosts_count)— team_id is not a leading column, so team‑scoped queries will do a residual filter and may not use index ordering for hosts_count.
Action: add a covering index:
CREATE INDEX idx_kernel_host_counts_team_os_sw_hosts ON kernel_host_counts (team_id, os_version_id, software_id, hosts_count)
Keep the existing index until EXPLAINs confirm it’s safe to drop.
Please run EXPLAIN FORMAT=JSON for the vuln queries (e.g. the JOIN around server/datastore/mysql/operating_system_vulnerabilities.go ~lines 54–63 and the aggregation around ~lines 223–236) with and withoutteam_idand paste the outputs to confirm index selection/filesort.server/fleet/datastore.go (1)
1119-1121: Verify MySQL impl — dedupe present; add chunking for OS IN lists; team scoping present; confirm index usage
- Dedupe: confirmed in server/datastore/mysql/operating_system_vulnerabilities.go — SQL-level DISTINCT / GROUP_CONCAT(DISTINCT) + MIN(created_at) and app-level dedupe loop (
found := false).- Chunking: partial — CVE/meta queries are batched (use of
batch/metaArgs), but OS ID lists are passed directly into IN(...) viastrings.Repeatforlen(osIDs)/len(linuxOSIDs)(no sub-chunking). Add chunking for those IN lists to avoid huge parameter lists.- Team scoping: implemented —
teamFilter/khc.team_idappended whenteamID != nilin the same file.- Index: repo contains
idx_kernel_host_counts_os_version_software (os_version_id, software_id, hosts_count)(schema + migration). Run EXPLAIN on the kernel_host_counts JOIN queries to confirm the index is used and that predicates align with the index column order.server/service/hosts.go (3)
2171-2203: Performance optimization: Batch vulnerability lookup successfully implementedThe change from per-OS vulnerability queries to a single batch query via
ListVulnsByMultipleOSVersionsis an effective solution to the N+1 query problem that was causing high database load. This should significantly reduce database queries when teams have many OS versions (e.g., 60k+ hosts scenario mentioned in the issue).
2193-2202: Good defensive programming with slice initializationInitializing empty slices for
VulnerabilitiesandKernelsensures JSON responses don't containnullvalues, maintaining API consistency.
2181-2183: Verified: composite key format matches datastore implementation and testsListVulnsByMultipleOSVersions builds keys as NameOnly + "-" + Version (server/datastore/mysql/operating_system_vulnerabilities.go) and the batch tests use the same (server/datastore/mysql/operating_system_vulnerabilities_batch_test.go).
server/mock/datastore_mock.go (3)
2684-2686: DataStore fields added for the new mock; LGTM.Naming and pattern are consistent with the rest of the mocks.
800-801: Approve — mock func signature matches Datastore interfaceVerified: server/fleet/datastore.go declares ListVulnsByMultipleOSVersions with the same signature and the mock in server/mock/datastore_mock.go matches; LGTM.
6479-6484: LGTM — mock sets invoked flag and delegates; tests assign the func.
Assignments found in server/service/hosts_test.go (lines 1301, 1346); call sites include server/service/hosts.go:2174 and datastore tests — no action required.server/datastore/mysql/operating_system_vulnerabilities.go (2)
10-10: Import looks correct.common_mysql is used for batched CVE metadata fetches. No issues.
357-372: Composite index present; row-constructor usage acceptable
- UNIQUE KEY
idx_unique_os(name,version,arch,kernel_version,platform,display_version) exists in server/datastore/mysql/schema.sql (≈ line 1787) — this provides the (name, version) composite index.- MySQL supports tuple/row comparisons, so WHERE (name, version) IN ((?, ?), ...) can use that composite index.
- Verify migrations and deployed DBs actually create/keep this index (check migration SQLs and production schema).
server/datastore/mysql/operating_system_vulnerabilities_batch_test.go (2)
37-64: Happy path coverage looks good.Covers 3 OS versions, asserts presence and counts without depending on order. 👍
277-319: Kernel seeding path is solid.Creates title/software and counts atomically; ON DUP keys prevent flakes. 👍
| createIndexStmt := ` | ||
| CREATE INDEX idx_kernel_host_counts_os_version_software | ||
| ON kernel_host_counts (os_version_id, software_id, hosts_count) | ||
| ` | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Confirm column order matches hottest query predicates.
If the common pattern is WHERE os_version_id = ? AND hosts_count > 0 then joining on software_id, consider (os_version_id, hosts_count, software_id). Please EXPLAIN the affected vuln queries before finalizing.
Run to find affected SQL and review join/filter patterns:
🏁 Script executed:
#!/bin/bash
# Inspect queries touching kernel_host_counts
rg -n -C3 --type=go "kernel_host_counts" server/datastore/mysql | sed -n '1,200p'Length of output: 13999
Change index to (os_version_id, hosts_count, software_id)
Multiple vuln queries filter by kernel_host_counts.os_version_id = ? (or via operating_systems.name/version) AND kernel_host_counts.hosts_count > 0 then join on software_id — the current index (os_version_id, software_id, hosts_count) prevents using hosts_count for the range predicate. Update migration server/datastore/mysql/migrations/tables/20250918154557_AddKernelHostCountsIndexForVulnQueries.go to create: CREATE INDEX idx_kernel_host_counts_os_version_hosts_software ON kernel_host_counts (os_version_id, hosts_count, software_id). EXPLAIN the queries in server/datastore/mysql/operating_system_vulnerabilities.go (≈lines 51–62, 223–236, 470–476) and confirm the new index is used before merging.
🤖 Prompt for AI Agents
In
server/datastore/mysql/migrations/tables/20250918154557_AddKernelHostCountsIndexForVulnQueries.go
around lines 17–21, the created index currently lists columns as (os_version_id,
software_id, hosts_count) which prevents using hosts_count for the range
predicate; change the CREATE INDEX to name
idx_kernel_host_counts_os_version_hosts_software and order columns as
(os_version_id, hosts_count, software_id). Update the migration to create that
index (and if necessary drop the old index name), then run EXPLAIN on the vuln
queries in server/datastore/mysql/operating_system_vulnerabilities.go (approx
lines 51–62, 223–236, 470–476) to confirm the new index is chosen for the
os_version_id = ? AND hosts_count > 0 predicates before merging.
There was a problem hiding this comment.
@coderabbitai Are you sure? Please think more about this and give specific code examples where this different index would benefit.
There was a problem hiding this comment.
🧩 Analysis chain
Based on the script results, let me examine the actual query patterns:
@getvictor You raise a good point - let me analyze the actual query patterns more carefully. Looking at the SQL queries in the codebase, I need to examine whether the current index column order (os_version_id, software_id, hosts_count) is actually optimal or if my suggested change would provide benefit.
From the query patterns I can see, the typical WHERE clauses are:
os_version_id IN (...)- exact match on multiple valueshosts_count > 0- range predicate- Joins on
software_id
For a query like:
SELECT ... FROM kernel_host_counts khc
JOIN software_cve scv ON scv.software_id = khc.software_id
WHERE khc.os_version_id IN (?, ?, ?) AND khc.hosts_count > 0Current index (os_version_id, software_id, hosts_count):
- Can use
os_version_idfor the IN clause efficiently - Can use
software_idfor the join condition - Cannot efficiently use
hosts_count > 0for range filtering (it's the last column)
Proposed index (os_version_id, hosts_count, software_id):
- Can use
os_version_idfor the IN clause efficiently - Can use
hosts_count > 0for range filtering - Can still cover
software_idfor joins (though not for range filtering after the hosts_count predicate)
However, you're right to question this. If the join selectivity on software_id is more important than filtering on hosts_count > 0, or if most rows already have hosts_count > 0, then the current index might be better.
Could you run EXPLAIN on one of the kernel vulnerability queries with the current index to see the execution plan? This would help determine if the hosts_count > 0 filtering is actually a bottleneck or if the current index is performing well.
🏁 Script executed:
#!/bin/bash
# Extract the specific SQL queries that use kernel_host_counts to analyze WHERE clause patterns
echo "=== Finding SQL queries with kernel_host_counts ==="
rg -n -A 20 -B 5 --type=go "kernel_host_counts.*WHERE\|WHERE.*kernel_host_counts\|JOIN.*kernel_host_counts" server/datastore/mysql/
echo -e "\n=== Looking for hosts_count filtering patterns ==="
rg -n -A 10 -B 5 --type=go "hosts_count\s*>\s*0\|hosts_count\s*>\s*\?\|hosts_count\s*>\s*\$" server/datastore/mysql/
echo -e "\n=== Finding the specific queries mentioned in my previous analysis ==="
# Look at the specific line ranges I mentioned: 51-62, 223-236, 470-476
sed -n '45,70p' server/datastore/mysql/operating_system_vulnerabilities.go
echo -e "\n--- Lines 220-240 ---"
sed -n '220,240p' server/datastore/mysql/operating_system_vulnerabilities.go
echo -e "\n--- Lines 465-480 ---"
sed -n '465,480p' server/datastore/mysql/operating_system_vulnerabilities.go
Length of output: 2189
🏁 Script executed:
#!/bin/bash
# Let's look at the complete SQL queries to understand the join and filter patterns
echo "=== Complete SQL queries in operating_system_vulnerabilities.go ==="
cat server/datastore/mysql/operating_system_vulnerabilities.go | grep -A 30 -B 5 "kernel_host_counts"
Length of output: 8664
🏁 Script executed:
#!/bin/bash
# Let's examine the new batched query method to see how it uses the index
echo "=== Examining the new ListVulnsByMultipleOSVersions method ==="
ast-grep --pattern 'func (ds *Datastore) ListVulnsByMultipleOSVersions($$$) {
$$$
}'
Length of output: 62
@getvictor You're absolutely right to question this - let me provide specific analysis based on the actual query patterns in the code.
Looking at the three main queries that use kernel_host_counts:
Query 1 (ListVulnsByOsNameAndVersion):
FROM software_cve
JOIN kernel_host_counts khc ON khc.software_id = software_cve.software_id
JOIN operating_systems os ON os.os_version_id = khc.os_version_id
WHERE os.name = ? AND os.version = ?
AND khc.hosts_count > 0Query 2 (ListKernelsByOS):
FROM software s
JOIN kernel_host_counts khc ON khc.software_id = s.id
WHERE khc.os_version_id = ?
AND khc.hosts_count > 0Query 3 (New batch method ListVulnsByMultipleOSVersions):
FROM software_cve sc
JOIN kernel_host_counts khc ON khc.software_id = sc.software_id
WHERE khc.os_version_id IN (?, ?, ...)
AND khc.hosts_count > 0All queries follow the pattern: filter by os_version_id → filter by hosts_count > 0 → join on software_id.
However, you're right to be skeptical. The current index (os_version_id, software_id, hosts_count) might actually be optimal if:
- High join selectivity: If the join on
software_idis very selective (few matching software IDs), MySQL might prefer to use the index to satisfy the join first - Low hosts_count = 0 ratio: If most rows already have
hosts_count > 0, then filtering on this condition provides little benefit
The proposed index (os_version_id, hosts_count, software_id) would only be better if filtering hosts_count > 0 significantly reduces the result set before the join.
My recommendation: Keep the current index as-is. The join condition is likely more selective than the hosts_count > 0 filter, and changing the index order could actually hurt performance for the join operations. The current index supports both the exact match on os_version_id and efficient joins on software_id.
| } | ||
|
|
||
| if r.ResolvedInVersion != nil { | ||
| resolvedVersion := r.ResolvedInVersion // avoid address of range var field |
There was a problem hiding this comment.
nit: we shouldn't need this in go 1.22+
There was a problem hiding this comment.
@coderabbitai What do you think? You gave the recommendation to do this since we're referencing the internal field of the var.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check the Go module version and examine the specific code pattern
cat go.mod | grep "^go "
Length of output: 9
🏁 Script executed:
# Let's examine the exact code pattern in the operating_system_vulnerabilities.go file
rg -A10 -B5 "resolvedVersion.*ResolvedInVersion" server/datastore/mysql/operating_system_vulnerabilities.go
Length of output: 895
Let me examine the code structure to understand the exact types and context:
@getvictor You're absolutely right to question this! After examining the code, I believe mostlikelee is correct, and there's actually an issue with the current pattern regardless of Go version.
Looking at the code:
if r.ResolvedInVersion != nil {
resolvedVersion := r.ResolvedInVersion // avoid address of range var field
vuln.ResolvedInVersion = &resolvedVersion
}The problem is that this pattern is creating a double pointer:
r.ResolvedInVersionis already of type*string(a pointer to string)resolvedVersion := r.ResolvedInVersionmakesresolvedVersiona*string&resolvedVersioncreates a**string(pointer to pointer to string)
But vuln.ResolvedInVersion should expect a *string, not a **string.
In Go 1.22+, the loop variable semantics changed so that each iteration gets fresh variables, which means the original concern about taking addresses of range variable fields is no longer valid. The correct code should simply be:
if r.ResolvedInVersion != nil {
vuln.ResolvedInVersion = r.ResolvedInVersion
}The same issue appears to exist in the kernel vulnerabilities section as well. This pattern was likely introduced as a workaround for pre-Go 1.22 loop variable capture issues, but:
- It's creating incorrect double pointers
- It's no longer needed in Go 1.22+ due to the loop variable semantics changes
- Even for earlier Go versions, direct assignment of the pointer value would be safe
mostlikelee is correct - this workaround isn't needed in Go 1.22+, and the current implementation appears to be creating incorrect pointer types.
🏁 Script executed:
# Check the Go module version and examine the specific code pattern
cat go.mod | grep "^go "
Length of output: 9
🏁 Script executed:
# Let's examine the exact code pattern and surrounding context
rg -A10 -B5 "resolvedVersion.*ResolvedInVersion" server/datastore/mysql/operating_system_vulnerabilities.go
Length of output: 895
🏁 Script executed:
# Let's also check the struct definition for CVE to understand the expected types
rg -A20 -B5 "type.*CVE.*struct" --type=go
Length of output: 37041
🏁 Script executed:
# Check if there's a similar pattern elsewhere in the same function
rg -A3 -B3 "avoid address of range var" server/datastore/mysql/operating_system_vulnerabilities.go
Length of output: 341
@getvictor After examining the code and struct definitions more carefully, I need to revise my initial assessment. The pattern is actually correct for this specific use case, but the comment is misleading about the reason.
Looking at the data types involved:
- From the database query:
r.ResolvedInVersionis*string(nullable database field) - The target field
vuln.ResolvedInVersionis**string(double pointer from the CVE struct definition)
The pattern:
if r.ResolvedInVersion != nil {
resolvedVersion := r.ResolvedInVersion // avoid address of range var field
vuln.ResolvedInVersion = &resolvedVersion
}Is doing the correct pointer level conversion:
r.ResolvedInVersionis*stringresolvedVersion := r.ResolvedInVersioncreates a*stringvariable&resolvedVersioncreates**stringwhich matches the expected type
However, the comment is misleading. This isn't about Go 1.22 loop variable changes at all. The double pointer design in the CVE struct allows for three states:
- Field omitted (for free tier)
- Field present but null (
**stringwhere outer pointer is non-nil, inner is nil) - Field present with value (
**stringwhere both pointers are non-nil)
mostlikelee is right that the Go 1.22 loop variable concerns don't apply here since we're using Go 1.25.1, but the pattern is still needed for the correct type conversion from *string to **string.
The comment should be updated to reflect the real reason:
resolvedVersion := r.ResolvedInVersion // convert *string to **string for CVE struct
vuln.ResolvedInVersion = &resolvedVersionSo to answer your question: The pattern is necessary for the type conversion, but not because of loop variable capture issues.
There was a problem hiding this comment.
fair, forgot it's a doublepointer
There was a problem hiding this comment.
@coderabbitai so why would this not work?
vuln.ResolvedInVersion = &r.ResolvedInVersionThere was a problem hiding this comment.
@getvictor That's a great question! The direct approach vuln.ResolvedInVersion = &r.ResolvedInVersion wouldn't work due to a memory aliasing issue.
Here's the problem:
In the range loop for _, r := range osVulnResults, the variable r is reused for each iteration. When you do &r.ResolvedInVersion, you're taking the address of a field in that loop variable r.
Since r is the same memory location across all iterations (just with different values assigned), all the &r.ResolvedInVersion addresses would point to the exact same memory location. This means:
- All vulnerabilities would end up sharing the same
ResolvedInVersionpointer - After the loop completes, all of them would have the value from the last iteration
- You'd get incorrect/duplicate data across all vulnerabilities
The current pattern:
resolvedVersion := r.ResolvedInVersion // Creates a copy of the *string value
vuln.ResolvedInVersion = &resolvedVersion // Takes address of the copyCreates a fresh copy of the pointer value for each iteration, so each vulnerability gets its own unique pointer.
This is actually still related to range variable behavior, but it's about memory reuse rather than the closure capture issue that Go 1.22 fixed. The struct field address problem persists regardless of Go version.
So the current approach is correct and necessary to avoid the memory aliasing issue!
🧠 Learnings used
Learnt from: lucasmrod
PR: fleetdm/fleet#30550
File: server/datastore/mysql/policies_test.go:6368-6378
Timestamp: 2025-07-03T15:13:36.494Z
Learning: Go 1.22 introduced a significant change to loop variable semantics where each iteration of a for loop creates a new variable, automatically fixing the classic closure capture bug that existed in earlier Go versions. Code that captures loop variables in closures no longer needs explicit variable copying in Go 1.22+. This fix is only enabled if the module's go.mod file specifies go 1.22 or higher.
Learnt from: lucasmrod
PR: fleetdm/fleet#30550
File: server/datastore/mysql/policies_test.go:6368-6378
Timestamp: 2025-07-03T15:13:36.494Z
Learning: Go 1.22 introduced a significant change to loop variable semantics where each iteration of a for loop creates a new variable, automatically fixing the classic closure capture bug that existed in earlier Go versions. Code that captures loop variables in closures no longer needs explicit variable copying in Go 1.22+.
Resolves #33147 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Resolved MySQL performance regressions from 4.73.0/4.73.1 affecting OS versions and software titles views, improving load times and reducing timeouts. - Refactor - Optimized OS vulnerabilities fetching by batching multiple OS versions in a single request. - Added a supporting database index to speed kernel-related vulnerability queries. - Tests - Added comprehensive tests for multi-OS vulnerability retrieval, CVSS enrichment, team-scoped data, and service endpoint behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> (cherry picked from commit d6695bf)
Resolves #33147 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked table schema to confirm autoupdate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Bug Fixes - Resolved MySQL performance regressions from 4.73.0/4.73.1 affecting OS versions and software titles views, improving load times and reducing timeouts. - Refactor - Optimized OS vulnerabilities fetching by batching multiple OS versions in a single request. - Added a supporting database index to speed kernel-related vulnerability queries. - Tests - Added comprehensive tests for multi-OS vulnerability retrieval, CVSS enrichment, team-scoped data, and service endpoint behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> (cherry picked from commit d6695bf)
Resolves #33147
Checklist for submitter
If some of the following don't apply, delete the relevant line.
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Testing
Added/updated automated tests
Where appropriate, automated tests simulate multiple hosts and test for host isolation (updates to one hosts's records do not affect another)
QA'd all new/changed functionality manually
Database migrations
Summary by CodeRabbit
Bug Fixes
Refactor
Tests