Skip to content

Improved Windows MDM reconciler - #47071

Merged
getvictor merged 14 commits into
mainfrom
45635-reconciler
Jun 11, 2026
Merged

Improved Windows MDM reconciler#47071
getvictor merged 14 commits into
mainfrom
45635-reconciler

Conversation

@getvictor

@getvictor getvictor commented Jun 8, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #45635

Moved profile reconciler work from SQL to code, similar to what Apple MDM team did last sprint.

The Windows MDM loadtest for 40 profiles with 30K hosts looks much better.

  ┌──────────────────────────┬─────────────────────────────────────────┬───────────────────────────────────────────┐
  │                          │            Pre-fix baseline             │                This branch                │
  ├──────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────┤
  │ Transfer wall time       │ ~40–42 min                              │ ~15.5 min                                 │
  ├──────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────┤
  │ Per work tick            │ 215–257s (host-finding query dominated) │ ~48s (host-finding gone; now bulk writes) │
  ├──────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────┤
  │ Ticks > 30s (work ticks) │ ~all                                    │ ~all (16/17, ~48s)                        │
  ├──────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────┤
  │ Pacing governor          │ the host-finding query                  │ the 2000-host delivery cap + 30s interval │
  └──────────────────────────┴─────────────────────────────────────────┴───────────────────────────────────────────┘

The writer spikes briefly to 16 AAS, but has CPU headroom, so I'd say we can claim to support 40 profies on 30K hosts.

  ┌───────────────┬───────────────┬───────────────────────────┬───────────────────────────────────────┐
  │   Instance    │ CPU avg / max │ DBLoad avg / max (4 vCPU) │     Read / Write / Commit latency     │
  ├───────────────┼───────────────┼───────────────────────────┼───────────────────────────────────────┤
  │ writer -two   │ 67.9% / 74.9% │ 4.84 / 16.0               │ 0.28ms / 2.07ms / 10.6ms (max 12.2ms) │
  ├───────────────┼───────────────┼───────────────────────────┼───────────────────────────────────────┤
  │ reader -one   │ 46.1% / 52.7% │ 1.19 / 5.0                │ 1.59ms / — / —                        │
  ├───────────────┼───────────────┼───────────────────────────┼───────────────────────────────────────┤
  │ reader -three │ 65.4% / 70.1% │ 1.77 / 5.0                │ 1.72ms / — / —                        │
  └───────────────┴───────────────┴───────────────────────────┴───────────────────────────────────────┘

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

Testing

Summary by CodeRabbit

  • Refactor

    • Reworked Windows MDM reconciliation to a snapshot-based, batched drain-loop, improving responsiveness and reducing database load during large profile operations.
  • Performance / Reliability

    • Windows MDM profile changes now reach hosts faster; large team-wide profile additions/removals (including host transfers) complete more quickly with lower DB impact.
  • Chore / Configuration

    • Added tunables to control per-tick delivery caps and scan budgets.
  • Tests

    • Expanded end-to-end and property tests for install/remove, team/label gating, and multi-window drain behavior.

getvictor added 5 commits June 7, 2026 09:30
Pure refactor, no behavior change. First step for #45635 (Windows
batched in-memory reconciler).

- New server/mdm/reconcile package holds the include/exclude label
  handlers and the team+label applicability dispatcher. The Apple
  platform gate stays in the Apple wrapper since platform eligibility
  is platform-specific.
- New platform-neutral fleet types (MDMProfileLabelRef,
  MDMProfileIncludeMode, MDMLabeledEntity); the Apple names are now
  type aliases so existing code and tests are unchanged.
- BulkGetHostLabelMemberships moves from apple_mdm_batched.go to a
  neutral file; it was already platform-agnostic.
- The existing Apple label-scenario tests keep covering the shared
  logic through the delegating wrappers; the shared package also gets
  its own handler/dispatcher tests.
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

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

qodo-free-for-open-source-projects Bot commented Jun 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Remediation recommended

1. Host-vitals exclude timing drift 🐞 Bug ≡ Correctness
Description
The new Windows in-memory reconciler uses reconcile.EntityAppliesToHost, whose exclude-any timing
safeguard only applies to dynamic labels, but the legacy Windows desired-state SQL applied the
safeguard to all non-manual labels. This can cause profiles excluded by newly created host_vitals
labels to be installed before a host’s label state has been refreshed since label creation.
Code

server/mdm/microsoft/reconcile.go[R37-42]

+		for _, p := range teamProfiles {
+			if !reconcile.EntityAppliesToHost(p, host.EffectiveTeamID(), host.LabelUpdatedAt, labelsForHost) {
+				continue
+			}
+			desired[p.ProfileUUID] = p
+		}
Evidence
The legacy Windows desired-state SQL explicitly applies the "label created after host scan"
safeguard to all non-manual labels (label_membership_type <> 1), which includes host_vitals
(value 2). After this PR, Windows desired-state evaluation runs through
reconcile.EntityAppliesToHost and HandlerExcludeAny, which only performs that timing check for
dynamic labels (value 0), so host_vitals exclude labels no longer trigger the same safeguard.

server/datastore/mysql/microsoft_mdm.go[2505-2545]
server/fleet/labels.go[94-106]
server/mdm/reconcile/reconcile.go[86-115]
server/mdm/microsoft/reconcile.go[31-42]

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

## Issue description
Windows profile reconciliation now computes desired state in memory via `reconcile.EntityAppliesToHost`.
The legacy Windows SQL desired-state query (exclude-any-only arm) treats *all non-manual* labels (`label_membership_type <> 1`) as requiring a post-creation host label scan (`host.label_updated_at >= label.created_at`) before the host can be considered “safe” to install a profile that’s gated by an exclude label.
The shared in-memory handler `HandlerExcludeAny` only applies that timing safeguard to **dynamic** labels (`LabelMembershipTypeDynamic`) and skips it for `host_vitals`. This creates a behavior drift: Windows profiles excluded via `host_vitals` labels may be installed earlier than under the legacy SQL.
### Issue Context
- Legacy Windows behavior is encoded in `windowsMDMProfilesDesiredStateQuery` (exclude-any-only arm).
- New Windows reconciler uses shared dispatcher via `reconcile.EntityAppliesToHost`, which relies on `HandlerExcludeAny`.
### Fix Focus Areas
- server/mdm/reconcile/reconcile.go[86-115]
- server/datastore/mysql/microsoft_mdm.go[2505-2545]
- server/mdm/reconcile/reconcile_test.go[1-220]
### Suggested fix
1. Decide the intended semantics for `LabelMembershipTypeHostVitals` in exclude-any timing:
- If Windows must match legacy SQL: update `HandlerExcludeAny` to apply the timing guard to all **non-manual** labels (i.e., `LabelMembershipType != Manual`) rather than only `Dynamic`.
- If the shared handler’s current behavior is intended: update the legacy Windows SQL exclude-any-only arm to align (replace `<> 1` with `= 0`), and ensure any callers still using that SQL are consistent.
2. Add/extend a unit test in `server/mdm/reconcile/reconcile_test.go` to cover an exclude-any label with `LabelMembershipTypeHostVitals` created after `hostLabelUpdatedAt`, asserting the chosen behavior explicitly.

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


Grey Divider

Qodo Logo

Base automatically changed from victor/45635-shared-reconcile-primitives to main June 8, 2026 11:28
@getvictor getvictor changed the title 45635 reconciler Improved Windows MDM reconciler Jun 8, 2026
# Conflicts:
#	server/mdm/reconcile/reconcile_test.go

Copilot AI 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.

Pull request overview

This PR reworks the Windows MDM configuration profile reconciler to eliminate the expensive “pending host” set-difference query by switching to an indexed snapshot read + in-memory diff, then draining multiple scan windows per cron tick subject to a delivery cap and a wall-clock scan budget (per #45635).

Changes:

  • Refactors ReconcileWindowsProfiles to page through enrolled Windows hosts via GetWindowsProfileReconcileSnapshot, compute install/remove deltas in memory, and drain successive windows within a tick.
  • Adds a new MySQL snapshot loader for the batched Windows reconciler and a shared in-memory delta computation implementation for Windows profiles.
  • Expands unit/property tests to cover cursor semantics, delivery-cap throttling, and scan-budget halting.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server/service/reconcile_windows_profiles_property_test.go Updates PBT fakes to drive the new snapshot-based reconciler and budget/cap tunables.
server/service/microsoft_mdm.go Refactors Windows profile reconcile tick into a drain loop with delivery cap + scan budget; extracts legacy batch execution into a helper.
server/service/microsoft_mdm_test.go Updates reconciler test harness to use snapshots; adds tests for delivery cap and scan budget behavior.
server/mock/datastore_mock.go Extends the mock datastore with GetWindowsProfileReconcileSnapshot.
server/mdm/reconcile/reconcile_test.go Fixes a slice-aliasing pitfall in HasBrokenLabel test entity helper.
server/mdm/microsoft/reconcile.go Introduces Windows in-memory delta computation mirroring the legacy SQL diff rules.
server/mdm/microsoft/reconcile_test.go Adds unit tests covering install/remove diff rules, team scoping, and label gating for Windows deltas.
server/fleet/windows_mdm.go Adds reconcile snapshot DTOs (WindowsHostReconcileInfo, WindowsProfileForReconcile) and labeled-entity impl.
server/fleet/datastore.go Adds the datastore interface method GetWindowsProfileReconcileSnapshot.
server/datastore/mysql/microsoft_mdm_batched.go Implements the MySQL snapshot loader: host window + profiles/labels + host label memberships + current windows profile rows.
changes/45635-windows-batched-reconciler Adds changelog entry for the Windows MDM reconciler redesign.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/service/microsoft_mdm.go
Comment thread server/mdm/microsoft/reconcile.go Outdated
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3cafa08b-0917-468d-8169-debcd7e89654

📥 Commits

Reviewing files that changed from the base of the PR and between 7abbaec and 1cad37d.

📒 Files selected for processing (1)
  • changes/45635-windows-batched-reconciler
✅ Files skipped from review due to trivial changes (1)
  • changes/45635-windows-batched-reconciler

Walkthrough

This PR refactors Windows MDM profile reconciliation to eliminate the query bottleneck (#45635) by replacing a fleet-wide set-difference query with in-memory delta computation from cursor-paginated snapshots. The reconciliation loop now reads snapshots in a drain model, computing install/remove deltas per snapshot while respecting per-tick delivery caps and scan budgets, and persisting the cursor only after successful delivery. Supporting changes include new reconciliation data types implementing team and label gating; MySQL snapshot queries with host window, profile, and label-membership loading; mock and test updates to exercise the new snapshot path and cursor advance behavior.

Possibly related PRs

  • fleetdm/fleet#47032: Refactors shared MDM label applicability primitives (EntityAppliesToHost), which the new Windows delta computation uses.
  • fleetdm/fleet#42206: Changes Windows MDM removal flow to enqueue <Delete> SyncML commands and mark host-profile rows as operation_type=remove; the new delta logic interacts with removal statuses.
  • fleetdm/fleet#44075: Modifies the Windows ReconcileWindowsProfiles batching/cursor flow that this PR further refactors into a snapshot-driven drain-loop.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Improved Windows MDM reconciler' is vague and doesn't clearly convey the main change, which is moving reconciler work from SQL to code for performance optimization. Consider a more specific title like 'Move Windows MDM reconciler work from SQL to code' or 'Optimize Windows MDM reconciliation by computing deltas in-memory'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is mostly complete with related issue, testing checklist items checked, changes file added, and manual QA confirmed. Load test results demonstrate the performance improvements achieved.
Linked Issues check ✅ Passed The PR successfully addresses issue #45635 by eliminating the slow ListNextPendingMDMWindowsHostUUIDs query through batched, in-memory reconciliation. Per-tick time reduced from 215-257s to ~48s; query P99 no longer the bottleneck.
Out of Scope Changes check ✅ Passed All changes are within scope of optimizing Windows MDM reconciliation. Minor test helper update to ReconcileWindowsProfiles is appropriately scoped for preventing potential slice backing-array mutations.
Docstring Coverage ✅ Passed Docstring coverage is 93.10% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 45635-reconciler

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 and usage tips.

@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.

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@server/datastore/mysql/microsoft_mdm_batched.go`:
- Around line 104-125: The SELECT is returning mcpl.label_id which remains
non-NULL when the LEFT JOIN misses, so deleted labels are treated as live;
update the projection in the labelStmt to return the joined label's id (use
lbl.id AS label_id) instead of mcpl.label_id so that missing labels produce NULL
and preserve the “broken label” semantics; make the same change for the other
occurrence of this query (the similar block around the second label query) and
ensure any downstream variable named ref or LabelID is still fed from this
projected column.

In `@server/service/microsoft_mdm_test.go`:
- Around line 685-693: The helper currently creates one profile per host by
incrementing teamID for each host, causing duplicate WindowsProfileForReconcile
entries for the same ProfileUUID; change the logic to deduplicate by
ProfileUUID: build a map from ProfileUUID to a single teamID and a single
fleet.WindowsProfileForReconcile, assign that same teamID to every
fleet.WindowsHostReconcileInfo that references that profile (ensure
WindowsHostReconcileInfo.TeamID points to the mapped teamID), and only append
one WindowsProfileForReconcile per unique ProfileUUID (use hostToProfile,
profiles slice, WindowsProfileForReconcile, and WindowsHostReconcileInfo to
locate and update the code).

In `@server/service/microsoft_mdm.go`:
- Around line 3541-3575: The current loop counts all workHosts toward
deliveredHosts even if executeWindowsProfileReconcileBatch skipped some hosts;
change executeWindowsProfileReconcileBatch to return the slice/set of host UUIDs
that were actually queued or terminally marked (e.g. []string or
map[string]struct{}) and update this call site to increment deliveredHosts by
the size of that returned set instead of len(workHosts); keep commitCursor =
advanceTo unchanged (cursor advancement stays independent of scheduling), and
update any other callers/tests of executeWindowsProfileReconcileBatch to accept
the new return value and propagate errors as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7fa70808-36ee-4dc9-a557-cf8869913504

📥 Commits

Reviewing files that changed from the base of the PR and between 72c5605 and 7544923.

📒 Files selected for processing (11)
  • changes/45635-windows-batched-reconciler
  • server/datastore/mysql/microsoft_mdm_batched.go
  • server/fleet/datastore.go
  • server/fleet/windows_mdm.go
  • server/mdm/microsoft/reconcile.go
  • server/mdm/microsoft/reconcile_test.go
  • server/mdm/reconcile/reconcile_test.go
  • server/mock/datastore_mock.go
  • server/service/microsoft_mdm.go
  • server/service/microsoft_mdm_test.go
  • server/service/reconcile_windows_profiles_property_test.go

Comment thread server/datastore/mysql/microsoft_mdm_batched.go Outdated
Comment thread server/service/microsoft_mdm_test.go Outdated
Comment thread server/service/microsoft_mdm.go Outdated
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.24242% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.10%. Comparing base (6a84d3a) to head (1cad37d).
⚠️ Report is 42 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/microsoft_mdm_batched.go 75.54% 27 Missing and 18 partials ⚠️
server/service/microsoft_mdm.go 90.90% 4 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #47071      +/-   ##
==========================================
+ Coverage   67.06%   67.10%   +0.04%     
==========================================
  Files        2893     2891       -2     
  Lines      225198   226215    +1017     
  Branches    11772    11768       -4     
==========================================
+ Hits       151018   151799     +781     
- Misses      60503    60651     +148     
- Partials    13677    13765      +88     
Flag Coverage Δ
backend 68.81% <84.24%> (+0.04%) ⬆️

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.

@getvictor
getvictor marked this pull request as ready for review June 9, 2026 09:08
@getvictor
getvictor requested a review from a team as a code owner June 9, 2026 09:08

@ksykulev ksykulev 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.

Both non-blocking comments.

// Delivery cap reached exactly at a window boundary.
return nil
case time.Now().After(deadline):
// Scan budget exhausted; resume next tick from cursor = advanceTo.

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.

Is it worth logging something here?

errMsg := "windows profile has mixed include label modes; ignoring include labels"
ds.logger.ErrorContext(ctx, errMsg, "profile_uuid", uuid, "team_id",
p.TeamID)
ctxerr.Handle(ctx, errors.New(errMsg))

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.

error or warning? I think the apple equivalent uses a warning.
ds.logger.WarnContext(ctx, "apple profile has mixed include label modes; ...", ...)

But non-blocking comment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I believe the intent here is this mixed-mode case should be impossible to reach, and the Apple equivalent should log an error as well. cc: @MagnusHJensen

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.

We can do that, the most important is just it does not get pushed to the device, and some kind of message is hit, but error sounds fine to me, I'll get a small PR up

@getvictor
getvictor merged commit b4dcea8 into main Jun 11, 2026
42 checks passed
@getvictor
getvictor deleted the 45635-reconciler branch June 11, 2026 06:03
getvictor added a commit that referenced this pull request Jun 11, 2026
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #46993 

Requires #47071 to merge first

Loadtest shows reduction of batch delete of 40 profiles for 30K hosts
down to ~3.9 seconds.

# 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] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Resolved timeout issues when removing large numbers of Windows
configuration profiles from teams with many hosts.

* **New Features**
* Windows profile deletions now process asynchronously in the
background, enabling faster API responses and consistent behavior with
profile delivery operations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@coderabbitai coderabbitai Bot mentioned this pull request Jun 30, 2026
6 tasks
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.

[top bottleneck] Windows MDM host-finding query is the dominant bottleneck in profile reconciliation

4 participants