Skip to content

45635 reconciler#47070

Closed
getvictor wants to merge 5 commits into
mainfrom
45635-reconciler
Closed

45635 reconciler#47070
getvictor wants to merge 5 commits into
mainfrom
45635-reconciler

Conversation

@getvictor

@getvictor getvictor commented Jun 8, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves

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.

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

  • Timeouts are implemented and retries are limited to avoid infinite loops

  • If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes

Testing

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

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.
  • Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.
  • Ensured the correct collation is explicitly set for character columns (COLLATE utf8mb4_unicode_ci).

New Fleet configuration settings

  • Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for GitOps-enabled settings:

  • Verified that the setting is exported via fleetctl generate-gitops
  • Verified the setting is documented in a separate PR to the GitOps documentation
  • Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional)
  • Verified that any relevant UI is disabled when GitOps mode is enabled

fleetd/orbit/Fleet Desktop

  • Verified compatibility with the latest released version of Fleet (see Must rule)
  • If the change applies to only one platform, confirmed that runtime.GOOS is used as needed to isolate changes
  • Verified that fleetd runs on macOS, Linux and Windows
  • Verified auto-update works from the released version of component to the new version (see tools/tuf/test)

Summary by CodeRabbit

  • Performance Improvements

    • Windows MDM profile reconciliation now processes changes more efficiently, reducing database load and latency during bulk fleet operations.
  • Platform Consistency

    • Label-based profile targeting logic is now unified across Apple and Windows platforms, ensuring consistent applicability behavior.

getvictor added 4 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 getvictor requested a review from Copilot June 8, 2026 10:39
@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 (2) 📘 Rule violations (0)

Grey Divider


Action required

1. Cursor advances without work 🐞 Bug ☼ Reliability
Description
The reconciler advances and persists the host UUID cursor even when a scanned window produced no
install/remove work, so if the scan budget ends mid-pass the persisted cursor can skip newly-created
work for hosts with UUIDs <= cursor until wraparound. This can significantly delay enforcement for
those hosts under large fleets or constrained scan budgets.
Code

server/service/microsoft_mdm.go[R3566-3592]

+		if len(toInstall) > 0 || len(toRemove) > 0 {
+			if eerr := executeWindowsProfileReconcileBatch(ctx, ds, logger, appConfig, toInstall, toRemove); eerr != nil {
+				err = eerr
+				return err
+			}
+		}
+		deliveredHosts += len(workHosts)
+
+		// Advance only after a successful execute.
+		commitCursor = advanceTo
+		cursor = advanceTo
+
+		switch {
+		case partial:
+			// Delivery cap hit mid-window; the un-delivered remainder resumes
+			// next tick from cursor = advanceTo.
+			return nil
+		case !fullBatch:
+			// Short window => end of the host space; reset for the next pass.
+			commitCursor = ""
+			return nil
+		case reconcileWindowsProfilesDeliveryCap > 0 && deliveredHosts >= reconcileWindowsProfilesDeliveryCap:
+			// Delivery cap reached exactly at a window boundary.
+			return nil
+		case time.Now().After(deadline):
+			// Scan budget exhausted; resume next tick from cursor = advanceTo.
+			return nil
Evidence
The code advances commitCursor unconditionally after each iteration (even if execute is skipped
due to empty deltas) and can return early on scan budget exhaustion, after which the deferred cursor
persistence writes the advanced value.

server/service/microsoft_mdm.go[3505-3511]
server/service/microsoft_mdm.go[3566-3592]

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

## Issue description
`ReconcileWindowsProfiles` advances `commitCursor`/`cursor` after each scanned window regardless of whether any work was delivered, and persists `commitCursor` at tick end on success. If the tick exits due to `reconcileWindowsProfilesScanBudget`, the persisted cursor can land past hosts that had no work *at scan time*; any new work created for hosts with UUIDs <= persisted cursor will not be discovered until the cursor wraps and resets.

## Issue Context
This is a behavioral regression in work discovery latency compared to the legacy “pending hosts” query approach. It is most visible on large fleets where a full scan cannot complete inside the scan budget.

## Fix Focus Areas
- server/service/microsoft_mdm.go[3505-3511]
- server/service/microsoft_mdm.go[3566-3592]

## Suggested fixes (choose one)
1) **Two-phase scan per tick**: before resuming from `entryCursor`, always scan a small window from the beginning (cursor="") to catch newly pending work in the prefix, then continue from `entryCursor` for progress.
2) **Persist cursor only when progress is meaningful**: when exiting due to scan budget, if `deliveredHosts == 0` (no work delivered this tick), avoid persisting the advanced cursor (or persist a separate “scan cursor” distinct from the “delivery cursor”).
3) **Hybrid discovery**: keep the scan cursor, but add a cheap “is there pending work before cursor?” check (e.g., bounded pending-host query) and, if yes, reset/resume appropriately.

The key requirement is: do not allow a successful, no-work scan to persist a cursor state that can hide newly pending work behind it for many ticks.

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



Remediation recommended

2. Redundant profile reloads 🐞 Bug ➹ Performance
Description
ReconcileWindowsProfiles reloads the full Windows profile+label set on every host-window iteration
via GetWindowsProfileReconcileSnapshot, repeating identical full-table reads multiple times per
tick. This increases DB read load and reduces drain throughput (more likely to hit the scan budget
before reaching later windows).
Code

server/service/microsoft_mdm.go[R3516-3537]

+	for {
+		hosts, allProfiles, hostLabels, currentByHost, serr := ds.GetWindowsProfileReconcileSnapshot(ctx, cursor, reconcileWindowsProfilesBatchSize)
+		if serr != nil {
+			err = ctxerr.Wrap(ctx, serr, "loading windows profile reconcile snapshot")
+			return err
+		}
+
+		if len(hosts) == 0 {
+			// Reached the end of the host space (or empty fleet): reset the
+			// cursor so the next pass restarts from the beginning.
+			commitCursor = ""
+			return nil
+		}
+
+		profilesByTeam := make(map[uint][]*fleet.WindowsProfileForReconcile, 4)
+		profilesWithBrokenLabel := make(map[string]struct{})
+		for _, p := range allProfiles {
+			profilesByTeam[p.TeamID] = append(profilesByTeam[p.TeamID], p)
+			if p.HasBrokenLabel() {
+				profilesWithBrokenLabel[p.ProfileUUID] = struct{}{}
+			}
+		}
Evidence
The drain loop calls the snapshot function repeatedly, and the snapshot function explicitly loads
allProfiles each time before computing labelIDs and host memberships.

server/service/microsoft_mdm.go[3516-3537]
server/datastore/mysql/microsoft_mdm_batched.go[272-307]

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

## Issue description
`ReconcileWindowsProfiles` drains multiple host windows per tick, but each loop iteration calls `GetWindowsProfileReconcileSnapshot`, which in turn loads *every* Windows profile and its label assignments. This causes repeated identical reads (profiles + labels) per window.

## Issue Context
This is a performance/throughput regression risk: the host window changes each iteration, but the profile universe typically does not. Re-loading the full profile set per window amplifies DB work and can consume the scan budget.

## Fix Focus Areas
- server/service/microsoft_mdm.go[3516-3539]
- server/datastore/mysql/microsoft_mdm_batched.go[282-339]

## Suggested direction
- Split the snapshot into (A) “static per tick” reads (all profiles + label assignments + derived labelIDs) and (B) “per window” reads (hosts window + host↔label memberships for those labelIDs + current host profile rows).
- In the drain loop, load (A) once per tick and reuse it for each window.
- Keep transactional consistency per window for (B); consistency between (A) and (B) across windows is typically acceptable for a reconciler, but if strict snapshot consistency is required, consider a version/checksum guard or reload (A) only when profile tables change.

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


Grey Divider

Qodo Logo

Comment thread server/service/microsoft_mdm.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 addresses #45635 by replacing the expensive Windows MDM “pending hosts” set-difference query with an indexed snapshot + in-memory diff approach, and introducing a shared, platform-neutral label/team applicability dispatcher used by both Apple and Windows reconcilers.

Changes:

  • Reworked Windows profile reconciliation to page through enrolled Windows hosts via GetWindowsProfileReconcileSnapshot, compute deltas in memory, and drain multiple windows per tick (bounded by a delivery cap and scan-time budget).
  • Introduced server/mdm/reconcile as the shared dispatcher for team/include/exclude label semantics; refactored Apple to delegate to it.
  • Added/updated unit + property-based tests for cursor protocol, delta computation, and delivery-cap throttling.

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 model to use snapshot-based pagination and new budget tunables.
server/service/microsoft_mdm.go Implements snapshot-driven drain loop with delivery cap + scan budget; extracts legacy batch execution into helper.
server/service/microsoft_mdm_test.go Updates mocks to provide snapshot inputs; adds delivery-cap throttling test.
server/mock/datastore_mock.go Adds mock support for GetWindowsProfileReconcileSnapshot.
server/mdm/reconcile/reconcile.go New platform-neutral dispatcher + include/exclude handlers.
server/mdm/reconcile/reconcile_test.go Unit tests for shared dispatcher/handlers.
server/mdm/microsoft/reconcile.go New in-memory desired-vs-current diff for Windows profiles.
server/mdm/microsoft/reconcile_test.go Unit tests for Windows delta rules, team gating, label matrix.
server/mdm/apple/reconcile.go Refactors Apple wrapper to call shared dispatcher after Apple platform gate.
server/mdm/apple/reconcile_test.go Removes duplicated handler tests; adds Apple-wrapper threading assertions.
server/fleet/windows_mdm.go Adds WindowsHostReconcileInfo / WindowsProfileForReconcile types used by snapshot + diff.
server/fleet/mdm_reconcile.go Adds platform-neutral include-mode + label-ref primitives and MDMLabeledEntity.
server/fleet/datastore.go Adds GetWindowsProfileReconcileSnapshot to datastore interface.
server/fleet/apple_mdm.go Aliases Apple label/include types to platform-neutral primitives.
server/datastore/mysql/microsoft_mdm_batched.go Implements snapshot loading in a read-only transaction for Windows reconciling.
server/datastore/mysql/mdm_label_memberships.go Extracts reusable host↔label membership bulk lookup helper.
server/datastore/mysql/apple_mdm_batched.go Removes duplicated label-membership helper now extracted to new file.
changes/45635-windows-batched-reconciler Release note for Windows reconciler performance change.
changes/45635-shared-reconcile-primitives Release note for shared reconcile primitives refactor.

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

Comment thread server/service/microsoft_mdm_test.go
Comment thread server/service/microsoft_mdm.go
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR refactors MDM profile label-targeting rules into a platform-neutral shared package used by both Apple and Windows reconcilers, and replaces Windows MDM's fleet-wide query-based host finding with a snapshot-driven in-memory batch-processing model. The refactoring extracts include-all/any and exclude-any label gating logic into a shared dispatcher, aligns Apple types to those definitions, and implements Windows-specific reconciliation data structures and delta computation. Windows reconciliation shifts from querying pending hosts to iterating snapshots of enrolled-host batches, computing install/remove deltas in memory, and draining work within a per-tick delivery cap to reduce single-change latency and eliminate the host-discovery query bottleneck.

Possibly related PRs

  • fleetdm/fleet#45573: Adds Apple-specific include/exclude label-gating handlers directly to server/mdm/apple/reconcile.go, which this PR removes and replaces with delegation to the new shared dispatcher.
  • fleetdm/fleet#44075: Earlier cursor-based batching changes to Windows reconciliation loop; this PR builds on the cursor model by switching from pending-host listing to snapshot-based paging.
  • fleetdm/fleet#46437: Adds API and GitOps support for combining include-any/all with labels_exclude_any; the shared reconciliation gates in this PR implement the actual host applicability decisions for those combined label policies.
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only the template checklist with no implementation details, summary of changes, or explanation of the work performed. Add a substantive description summarizing the key changes: the shared label reconciliation module, the snapshot-based Windows reconciler redesign, and how these changes address issue #45635's performance bottleneck.
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "45635 reconciler" is vague and generic, using only an issue number and a broad term without describing the specific change. Revise the title to be more specific and descriptive, e.g., "Refactor Windows MDM reconciler to use batched snapshot-based approach" or "Extract platform-neutral MDM label reconciliation logic and optimize Windows reconciler".
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully addresses issue #45635 by refactoring the Windows MDM reconciler from a full set-difference query approach to a batched snapshot-based approach with in-memory delta computation, eliminating the slow host-finding query.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objectives in #45635: extracting platform-neutral MDM reconcile logic, implementing snapshot-based Windows reconciliation, and removing the problematic host-finding query.

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

✨ 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 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: 1

🤖 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/mdm/reconcile/reconcile_test.go`:
- Around line 23-30: The HasBrokenLabel method currently uses
append(e.includeLabels, e.excludeLabels...) which can mutate e.includeLabels;
change the implementation to avoid mutating state by iterating the two slices
separately (first loop over e.includeLabels, then loop over e.excludeLabels) and
return true if any LabelID == nil, otherwise return false; reference the
testEntity.HasBrokenLabel method and the includeLabels and excludeLabels fields
when making the change.
🪄 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: faa47e95-b566-401c-a14e-7f4fae27d168

📥 Commits

Reviewing files that changed from the base of the PR and between 836695a and 20ac6ec.

📒 Files selected for processing (19)
  • changes/45635-shared-reconcile-primitives
  • changes/45635-windows-batched-reconciler
  • server/datastore/mysql/apple_mdm_batched.go
  • server/datastore/mysql/mdm_label_memberships.go
  • server/datastore/mysql/microsoft_mdm_batched.go
  • server/fleet/apple_mdm.go
  • server/fleet/datastore.go
  • server/fleet/mdm_reconcile.go
  • server/fleet/windows_mdm.go
  • server/mdm/apple/reconcile.go
  • server/mdm/apple/reconcile_test.go
  • server/mdm/microsoft/reconcile.go
  • server/mdm/microsoft/reconcile_test.go
  • server/mdm/reconcile/reconcile.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
💤 Files with no reviewable changes (1)
  • server/datastore/mysql/apple_mdm_batched.go

Comment on lines +23 to +30
func (e *testEntity) HasBrokenLabel() bool {
for _, l := range append(e.includeLabels, e.excludeLabels...) {
if l.LabelID == nil {
return true
}
}
return false
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid mutating includeLabels inside HasBrokenLabel.

append(e.includeLabels, e.excludeLabels...) can mutate e.includeLabels when capacity allows, which may leak state across assertions. Iterate both slices separately.

Suggested fix
 func (e *testEntity) HasBrokenLabel() bool {
-	for _, l := range append(e.includeLabels, e.excludeLabels...) {
+	for _, l := range e.includeLabels {
+		if l.LabelID == nil {
+			return true
+		}
+	}
+	for _, l := range e.excludeLabels {
 		if l.LabelID == nil {
 			return true
 		}
 	}
 	return false
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (e *testEntity) HasBrokenLabel() bool {
for _, l := range append(e.includeLabels, e.excludeLabels...) {
if l.LabelID == nil {
return true
}
}
return false
}
func (e *testEntity) HasBrokenLabel() bool {
for _, l := range e.includeLabels {
if l.LabelID == nil {
return true
}
}
for _, l := range e.excludeLabels {
if l.LabelID == nil {
return true
}
}
return false
}
🤖 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/mdm/reconcile/reconcile_test.go` around lines 23 - 30, The
HasBrokenLabel method currently uses append(e.includeLabels, e.excludeLabels...)
which can mutate e.includeLabels; change the implementation to avoid mutating
state by iterating the two slices separately (first loop over e.includeLabels,
then loop over e.excludeLabels) and return true if any LabelID == nil, otherwise
return false; reference the testEntity.HasBrokenLabel method and the
includeLabels and excludeLabels fields when making the change.

@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.15663% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.00%. Comparing base (fbabd07) to head (7544923).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/microsoft_mdm_batched.go 80.54% 20 Missing and 16 partials ⚠️
server/service/microsoft_mdm.go 93.50% 3 Missing and 2 partials ⚠️
server/datastore/mysql/mdm_label_memberships.go 87.87% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #47070      +/-   ##
==========================================
- Coverage   67.04%   67.00%   -0.04%     
==========================================
  Files        2875     2881       +6     
  Lines      225133   225881     +748     
  Branches    11780    11780              
==========================================
+ Hits       150931   151344     +413     
- Misses      60530    60851     +321     
- Partials    13672    13686      +14     
Flag Coverage Δ
backend 68.69% <89.15%> (-0.06%) ⬇️

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.

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