Skip to content

CP: Optimize ReparseWindowsHostCertificates migration (#50228) - #50263

Draft
sharon-fdm wants to merge 2 commits into
rc-minor-fleet-v4.90.0from
sharon-fdm/cp-fix-reparse-certs-490
Draft

CP: Optimize ReparseWindowsHostCertificates migration (#50228)#50263
sharon-fdm wants to merge 2 commits into
rc-minor-fleet-v4.90.0from
sharon-fdm/cp-fix-reparse-certs-490

Conversation

@sharon-fdm

@sharon-fdm sharon-fdm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Related issue: #50228

Cherry-pick of #50262 into the 4.90 RC branch.

Checklist for submitter

  • Changes file added for user-visible changes in changes/.
  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

Testing

  • Added/updated automated tests
  • QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

  • Confirmed that the fix is not expected to adversely impact load test results
  • Alerted the release DRI if additional load testing is needed

Summary

The ReparseWindowsHostCertificates migration caused ~14 minutes of sustained full DB load (CPU 89.75%, threads 2x vCPU count) during the 4.89 to 4.90 upgrade at 100K hosts.

Root cause: Each batch of 1,000 cert IDs re-joined the hosts table to filter by platform = 'windows'.

Fix: Collect Windows host IDs once upfront, then soft-delete certs in batches by host_id without re-joining.

Local benchmark: 1,000 Windows hosts x 20 certs (20K rows) completes in 445ms (was proportionally ~14 min at 100K).

Test plan

  • TestUp_20260723181402 passes (correctness)
  • TestUp_20260723181402_Performance passes in 445ms (performance)
  • @andrey-fleet load test 4.89 to 4.90 migration with this fix

Summary by CodeRabbit

  • Bug Fixes

    • Improved the Windows host certificate migration during upgrades.
    • Reduced sustained database load and improved migration performance when reprocessing certificates.
    • Ensured only applicable Windows certificates are reprocessed, while certificates on other platforms remain unaffected.
  • Tests

    • Added performance coverage to verify migration completion and certificate handling under a realistic workload.

The original migration re-joined the hosts table on every batch of
1000 cert IDs, causing ~14 minutes of sustained full DB load at 100K
hosts. Instead, collect all Windows host IDs once upfront and then
soft-delete certs in batches by host_id without the repeated join.
Inserts 1,000 Windows hosts x 20 certs (20K rows) and verifies the
migration completes in under 30s. Measured at 445ms with the fix,
vs the original which took ~14 minutes at 100K hosts.
@getvictor

Copy link
Copy Markdown
Member

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member

/agentic_review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.18182% with 7 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (rc-minor-fleet-v4.90.0@26196af). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...s/20260723181402_ReparseWindowsHostCertificates.go 68.18% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@                    Coverage Diff                    @@
##             rc-minor-fleet-v4.90.0   #50263   +/-   ##
=========================================================
  Coverage                          ?   67.89%           
=========================================================
  Files                             ?     3890           
  Lines                             ?   248287           
  Branches                          ?    12966           
=========================================================
  Hits                              ?   168575           
  Misses                            ?    64496           
  Partials                          ?    15216           
Flag Coverage Δ
backend 69.30% <68.18%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

// The test uses 1,000 Windows hosts x 20 certs each = 20,000 cert rows.
// This is enough to exercise the batching logic (hostBatchSize=500) while
// staying fast enough for CI (~seconds, not minutes).
func TestUp_20260723181402_Performance(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bench test doesn't need to be checked in, right?

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Flaky perf test in CI 🐞 Bug ☼ Reliability
Description
TestUp_20260723181402_Performance seeds ~25k rows using thousands of single-row INSERTs and enforces
a fixed <30s wall-clock threshold for applyNext(), which can slow down CI and intermittently fail on
slower/busy MySQL runners even when the migration is correct. Because recent migration tests run by
default, this test will execute in CI for the current release window.
Code

server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go[R80-105]

+	// Run the migration and time it.
+	start := time.Now()
+	applyNext(t, db)
+	elapsed := time.Since(start)
+	t.Logf("Migration completed in %s for %d Windows hosts x %d certs = %d cert rows",
+		elapsed, numWindowsHosts, certsPerHost, numWindowsHosts*certsPerHost)
+
+	// Verify: all Windows osquery certs are soft-deleted.
+	var windowsLive int
+	require.NoError(t, db.Get(&windowsLive, `
+		SELECT COUNT(*) FROM host_certificates hc
+		JOIN hosts h ON h.id = hc.host_id
+		WHERE h.platform = 'windows' AND hc.origin = 'osquery' AND hc.deleted_at IS NULL`))
+	require.Equal(t, 0, windowsLive, "all Windows osquery certs should be soft-deleted")
+
+	// Verify: all macOS certs are untouched.
+	var macLive int
+	require.NoError(t, db.Get(&macLive, `
+		SELECT COUNT(*) FROM host_certificates hc
+		JOIN hosts h ON h.id = hc.host_id
+		WHERE h.platform = 'darwin' AND hc.origin = 'osquery' AND hc.deleted_at IS NULL`))
+	require.Equal(t, numMacHosts*certsPerHost, macLive, "macOS certs should be untouched")
+
+	// At 1,000 hosts x 20 certs, the migration should complete in under 30 seconds.
+	// The original implementation took proportionally ~14 minutes at 100K hosts.
+	require.Less(t, elapsed, 30*time.Second, "migration took too long, possible performance regression")
Evidence
The new test seeds data with many per-row INSERTs and asserts a strict runtime bound; migration
tests are not skipped for recent timestamps, so this will run in CI for this RC window.

server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go[20-72]
server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go[80-105]
server/datastore/mysql/migrations/tables/migration_test.go[92-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
A performance-focused test with heavy DB seeding and a hard wall-clock threshold runs as a normal `go test`, increasing CI time and risking environment-dependent failures.

### Issue Context
Migration tests run by default for recent migrations (<60 days old), so this test will execute in standard CI.

### Fix Focus Areas
- server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go[20-106]
- server/datastore/mysql/migrations/tables/migration_test.go[92-106]

### Suggested fix options (pick one)
1) Convert to a benchmark (`func Benchmark...`) and run it only under `go test -bench` with `-run '^$'`.
2) Keep it as a test but gate it behind an opt-in env var (e.g., `if os.Getenv("FLEET_RUN_PERF_TESTS") == "" { t.Skip(...) }`) and/or `testing.Short()`.
3) If keeping it in the default suite, remove the strict wall-clock assertion and/or significantly reduce seeding cost (multi-row INSERTs / batch inserts) to minimize CI impact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. RowsAffected error ignored 🐞 Bug ◔ Observability
Description
softDeleteWindowsHostCertsForReparse ignores the error from result.RowsAffected(), so if the driver
can’t return an affected-row count the migration will still succeed but progress reporting can be
incorrect because increment() won’t be called for updated rows. This regresses the prior
deterministic per-row increment behavior used by incrementalMigrationStep to compute progress
percentages.
Code

server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates.go[R73-74]

+		affected, _ := result.RowsAffected()
+		for range affected {
Evidence
The migration’s progress is derived from increment() calls; ignoring RowsAffected() errors can
silently under-count progress when affected rows can’t be determined.

server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates.go[62-76]
server/datastore/mysql/migrations/tables/migration.go[43-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`RowsAffected()` errors are currently ignored when driving `increment()` calls. If the affected-row count cannot be obtained (or is unknown), progress output becomes misleading.

### Issue Context
`incrementalMigrationStep` computes and logs progress based on how many times `increment()` is invoked.

### Fix Focus Areas
- server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates.go[62-76]
- server/datastore/mysql/migrations/tables/migration.go[43-92]

### Suggested fix
- Capture and handle `RowsAffected()` error:
 - `affected, err := result.RowsAffected()`
 - If `err != nil` (or `affected < 0`), fall back to a deterministic count for that host batch (e.g., `SELECT COUNT(*) FROM host_certificates WHERE host_id IN (?) AND origin='osquery' AND deleted_at IS NULL` before the UPDATE) and use that count to call `increment()`.
 - Otherwise, loop `affected` times and call `increment()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +73 to +74
affected, _ := result.RowsAffected()
for range affected {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

1. Rowsaffected error ignored 🐞 Bug ◔ Observability

softDeleteWindowsHostCertsForReparse ignores the error from result.RowsAffected(), so if the driver
can’t return an affected-row count the migration will still succeed but progress reporting can be
incorrect because increment() won’t be called for updated rows. This regresses the prior
deterministic per-row increment behavior used by incrementalMigrationStep to compute progress
percentages.
Agent Prompt
### Issue description
`RowsAffected()` errors are currently ignored when driving `increment()` calls. If the affected-row count cannot be obtained (or is unknown), progress output becomes misleading.

### Issue Context
`incrementalMigrationStep` computes and logs progress based on how many times `increment()` is invoked.

### Fix Focus Areas
- server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates.go[62-76]
- server/datastore/mysql/migrations/tables/migration.go[43-92]

### Suggested fix
- Capture and handle `RowsAffected()` error:
  - `affected, err := result.RowsAffected()`
  - If `err != nil` (or `affected < 0`), fall back to a deterministic count for that host batch (e.g., `SELECT COUNT(*) FROM host_certificates WHERE host_id IN (?) AND origin='osquery' AND deleted_at IS NULL` before the UPDATE) and use that count to call `increment()`.
  - Otherwise, loop `affected` times and call `increment()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +80 to +105
// Run the migration and time it.
start := time.Now()
applyNext(t, db)
elapsed := time.Since(start)
t.Logf("Migration completed in %s for %d Windows hosts x %d certs = %d cert rows",
elapsed, numWindowsHosts, certsPerHost, numWindowsHosts*certsPerHost)

// Verify: all Windows osquery certs are soft-deleted.
var windowsLive int
require.NoError(t, db.Get(&windowsLive, `
SELECT COUNT(*) FROM host_certificates hc
JOIN hosts h ON h.id = hc.host_id
WHERE h.platform = 'windows' AND hc.origin = 'osquery' AND hc.deleted_at IS NULL`))
require.Equal(t, 0, windowsLive, "all Windows osquery certs should be soft-deleted")

// Verify: all macOS certs are untouched.
var macLive int
require.NoError(t, db.Get(&macLive, `
SELECT COUNT(*) FROM host_certificates hc
JOIN hosts h ON h.id = hc.host_id
WHERE h.platform = 'darwin' AND hc.origin = 'osquery' AND hc.deleted_at IS NULL`))
require.Equal(t, numMacHosts*certsPerHost, macLive, "macOS certs should be untouched")

// At 1,000 hosts x 20 certs, the migration should complete in under 30 seconds.
// The original implementation took proportionally ~14 minutes at 100K hosts.
require.Less(t, elapsed, 30*time.Second, "migration took too long, possible performance regression")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Flaky perf test in ci 🐞 Bug ☼ Reliability

TestUp_20260723181402_Performance seeds ~25k rows using thousands of single-row INSERTs and enforces
a fixed <30s wall-clock threshold for applyNext(), which can slow down CI and intermittently fail on
slower/busy MySQL runners even when the migration is correct. Because recent migration tests run by
default, this test will execute in CI for the current release window.
Agent Prompt
### Issue description
A performance-focused test with heavy DB seeding and a hard wall-clock threshold runs as a normal `go test`, increasing CI time and risking environment-dependent failures.

### Issue Context
Migration tests run by default for recent migrations (<60 days old), so this test will execute in standard CI.

### Fix Focus Areas
- server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go[20-106]
- server/datastore/mysql/migrations/tables/migration_test.go[92-106]

### Suggested fix options (pick one)
1) Convert to a benchmark (`func Benchmark...`) and run it only under `go test -bench` with `-run '^$'`.
2) Keep it as a test but gate it behind an opt-in env var (e.g., `if os.Getenv("FLEET_RUN_PERF_TESTS") == "" { t.Skip(...) }`) and/or `testing.Short()`.
3) If keeping it in the default suite, remove the strict wall-clock assertion and/or significantly reduce seeding cost (multi-row INSERTs / batch inserts) to minimize CI impact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The Windows host certificate re-parse migration now collects Windows host IDs once and soft-deletes matching osquery certificates in batches using host_id. Progress is based on affected rows. A new performance test seeds Windows and macOS hosts and certificates, verifies migration results by platform, and requires completion within 30 seconds. A changelog entry documents the optimization.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Clear, concise, and accurately summarizes the migration optimization.
Description check ✅ Passed Covers the issue link, summary, testing, and test plan; only the RC load-test confirmation and DB migration section are incomplete.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sharon-fdm/cp-fix-reparse-certs-490

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go (2)

50-72: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider batching cert inserts for faster test setup.

24,000 single-row INSERTs add setup overhead on top of the ~445ms migration being measured. A multi-row INSERT ... VALUES (...), (...), ... or wrapping the loop in an explicit transaction would reduce test runtime, though not essential since the test already passes comfortably under the 30s budget.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go`
around lines 50 - 72, The insertCerts helper performs one database INSERT per
certificate, adding unnecessary benchmark setup overhead. Update insertCerts to
batch certificate rows into multi-row INSERT statements or execute the loop
within an explicit transaction, while preserving the existing certificate values
and coverage for both Windows and macOS hosts.

50-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't exercise the origin/deleted_at scoping it's meant to validate.

All seeded Windows certs are origin='osquery' and live (deleted_at IS NULL), so the assertions at lines 93 and 101 would still pass even if the migration's origin = 'osquery' AND deleted_at IS NULL filter were accidentally dropped. Consider seeding a non-osquery-origin cert (e.g., MDM-origin) and an already-soft-deleted cert on a Windows host, then asserting both remain untouched, to actually pin down the filter's scoping.

As per path instructions, ensure SQL queries intended to scope a specific set of rows (here: only live osquery Windows certs) are backed by tests that would fail if that scoping regressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go`
around lines 50 - 101, Expand the certificate seeding in insertCerts or a nearby
helper to include Windows-host rows with a non-osquery origin and with
deleted_at already set, while retaining the live osquery rows. Update the
post-migration assertions to verify those scoped rows remain unchanged,
including counts or targeted queries for MDM-origin and pre-deleted
certificates, so the test fails if the migration processes anything beyond live
osquery certificates.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go`:
- Around line 50-72: The insertCerts helper performs one database INSERT per
certificate, adding unnecessary benchmark setup overhead. Update insertCerts to
batch certificate rows into multi-row INSERT statements or execute the loop
within an explicit transaction, while preserving the existing certificate values
and coverage for both Windows and macOS hosts.
- Around line 50-101: Expand the certificate seeding in insertCerts or a nearby
helper to include Windows-host rows with a non-osquery origin and with
deleted_at already set, while retaining the live osquery rows. Update the
post-migration assertions to verify those scoped rows remain unchanged,
including counts or targeted queries for MDM-origin and pre-deleted
certificates, so the test fails if the migration processes anything beyond live
osquery certificates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8787f336-a020-4b6d-b410-67d4f9255f6d

📥 Commits

Reviewing files that changed from the base of the PR and between 8d07a4c and 5c47b60.

📒 Files selected for processing (3)
  • changes/50228-fix-reparse-certs-migration
  • server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates.go
  • server/datastore/mysql/migrations/tables/20260723181402_ReparseWindowsHostCertificates_bench_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants