Skip to content

fleetd Windows MDM wake (push vs poll)#46594

Merged
getvictor merged 20 commits into
mainfrom
victor/43773-fleetd-mdm-wake
Jun 3, 2026
Merged

fleetd Windows MDM wake (push vs poll)#46594
getvictor merged 20 commits into
mainfrom
victor/43773-fleetd-mdm-wake

Conversation

@getvictor

@getvictor getvictor commented Jun 1, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #46567 and Resolves #46737

Solution for the agressive polling:

  • no WNS (although we could add it later as another avenue for notifications)
  • fleetd advertises a sync capability, persisted as mdm_windows_enrollments.fleetd_sync_capable
  • The management session relaxes the DMClient poll (poll_schedule_relaxed)
  • When an MDM command is queued, has_pending_commands flips, the next orbit check-in returns WindowsMDMSyncRequest, and fleetd runs deviceenroller to deliver it immediately
  • older fleetd versions keep the 1-minute poll

Docs: #46780

Changes to osquery_perf and any additional changes after loadtesting will be done in a separate PR.

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.

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

Testing

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

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

  • New Features

    • On-demand Windows MDM sync: servers can request immediate delivery of queued MDM commands to Windows clients; Orbit triggers client-side sync on Windows.
  • Enhancements

    • Orbit throttles per-device on-demand sync to avoid excessive runs.
    • Server reconciles and persists device poll schedule (fast vs relaxed) and exposes consolidated host MDM state (awaiting-configuration + has-pending-commands).
  • Tests

    • Added tests covering host config state, pending-command flows, poll-schedule toggling, and on-demand sync behavior.

@getvictor getvictor requested a review from Copilot June 1, 2026 19:24
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

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

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Deviceenroller lacks timeout 🐞 Bug ☼ Reliability
Description
TriggerWindowsMDMSync runs deviceenroller.exe via exec.Command/CombinedOutput with no timeout, so a
hung OMA-DM session can block the sync receiver goroutine indefinitely. Because
OrbitClient.RunConfigReceivers waits for all receiver goroutines to finish, a single hung sync
attempt can stall the entire config-receiver tick and prevent future ticks/receivers from running.
Code

orbit/pkg/update/execwinapi_windows.go[R215-227]

Evidence
The Windows sync receiver calls TriggerWindowsMDMSync synchronously, and Orbit’s receiver runner
waits for all receivers to complete; thus a hung subprocess blocks the whole periodic
config-receiver loop. The repo’s tablehelpers.Exec demonstrates the expected pattern of
context.WithTimeout + exec.CommandContext for bounded execution.

orbit/pkg/update/execwinapi_windows.go[198-229]
orbit/pkg/update/notifications.go[275-334]
client/orbit_client.go[276-334]
orbit/pkg/table/tablehelpers/exec.go[17-60]

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

## Issue description
`TriggerWindowsMDMSync` executes `deviceenroller.exe` without any deadline. If `deviceenroller` hangs (or takes excessively long), the on-demand sync receiver goroutine never returns, and `OrbitClient.RunConfigReceivers` will block forever waiting on its `WaitGroup`, effectively stalling future config polling and all other config receivers.
### Issue Context
- The on-demand sync receiver (`windowsMDMSyncConfigReceiver`) calls `TriggerWindowsMDMSync` synchronously.
- `OrbitClient.RunConfigReceivers` spawns receiver goroutines but then `wg.Wait()` blocks until *all* receivers return.
- The repo already has a standard pattern for enforcing timeouts on subprocess execution.
### Fix Focus Areas
- orbit/pkg/update/execwinapi_windows.go[209-229]
- orbit/pkg/update/notifications.go[301-334]
- client/orbit_client.go[276-334]
- orbit/pkg/table/tablehelpers/exec.go[17-60]
### Proposed fix
1. Wrap the `deviceenroller.exe` invocation in a context with a reasonable timeout (e.g. 30–120 seconds), and run it with `exec.CommandContext`.
2. Ensure the returned error clearly indicates timeout vs non-timeout failures (e.g. `context.DeadlineExceeded`).
3. (Optional) Consider making the timeout configurable via the receiver middleware frequency/args if needed for slower environments.

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


2. Replica lag hides commands ✓ Resolved 🐞 Bug ≡ Correctness
Description
GetOrbitConfig decides whether to set WindowsMDMSyncRequest from GetMDMWindowsHostConfigState, which
is reader-backed, so replica lag can omit the sync request even when commands were just queued. This
delays command delivery until the next scheduled poll, undermining the on-demand wake behavior.
Code

server/service/orbit.go[R578-599]

Evidence
GetMDMWindowsHostConfigState is implemented using ds.reader(ctx) and is documented as
reader-backed; GetOrbitConfig calls it to set WindowsMDMSyncRequest without forcing primary
routing, so replication lag can hide queued commands. The same service already forces primary reads
in other read-after-write/lag-sensitive flows, showing this is a known concern.

server/datastore/mysql/microsoft_mdm.go[198-247]
server/service/orbit.go[572-600]
server/service/orbit.go[149-164]

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

## Issue description
`Service.GetOrbitConfig` uses `GetMDMWindowsHostConfigState` to decide whether to send `windows_mdm_sync_request`, but the datastore method is reader-backed (`ds.reader(ctx)`). With replica lag, newly queued commands on primary may not be visible yet, so the server won’t request an on-demand OMA-DM session and command delivery latency regresses to the relaxed poll interval.
### Issue Context
This PR’s goal is low-latency command delivery without aggressive polling; missing the pending-command signal due to stale replica reads defeats that goal.
### Fix Focus Areas
- Ensure the pending-command check is primary-consistent on the orbit config path (e.g., wrap the call with `ctxdb.RequirePrimary(ctx, true)` when querying Windows MDM host state, or otherwise route that specific read to primary).
- server/service/orbit.go[578-600]
- server/datastore/mysql/microsoft_mdm.go[198-247]

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



Remediation recommended

3. Extra DB reads per session ✓ Resolved 🐞 Bug ➹ Performance
Description
getManagementResponse calls reconcileWindowsMDMPollSchedule on every trusted Windows MDM session
outside ESP, and reconcile always queries HostLiteByIdentifier and GetHostOrbitInfo to determine
capability even when the intended poll schedule already matches. For non-capable hosts that remain
on the fast poll, this adds two DB reads per check-in and can materially increase DB load during
rollout/for older fleetd versions.
Code

server/service/microsoft_mdm.go[R2131-2137]

Evidence
The reconciliation is invoked on every trusted management session outside ESP, and the capability
check performs two datastore reads before reconcile can early-return when no change is needed; the
fast poll is documented as 1 minute, so non-capable hosts will pay this cost every minute.

server/service/microsoft_mdm.go[2021-2029]
server/service/microsoft_mdm.go[2068-2075]
server/service/microsoft_mdm.go[2080-2098]
server/service/microsoft_mdm.go[2131-2138]

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

## Issue description
`reconcileWindowsMDMPollSchedule` calls `windowsMDMHostSupportsSync` before it can short-circuit on `desiredRelaxed == device.PollScheduleRelaxed`, which forces two datastore reads (`HostLiteByIdentifier`, `GetHostOrbitInfo`) on *every* trusted Windows MDM management session outside ESP. This is especially costly for hosts that are not capable of on-demand sync (they stay on the 1-minute poll), because the reconciliation becomes a steady-state no-op but still pays for the reads.
### Issue Context
- The poll schedule reconciliation runs for trusted sessions when `AwaitingConfiguration == None`.
- Capability is derived via host+orbit-info lookups.
### Fix Focus Areas
- server/service/microsoft_mdm.go[2021-2153]
- server/service/orbit.go[578-598]
- server/datastore/mysql/microsoft_mdm.go[212-241]
### Implementation direction (one of)
1) Persist a boolean (or last-known orbit version) on `mdm_windows_enrollments` (e.g. `supports_mdm_sync`) updated from the orbit config request capabilities header (which you already parse in `GetOrbitConfig`), and have `reconcileWindowsMDMPollSchedule` use that field instead of doing per-session host/orbit-info reads.
2) If you must keep orbit-info-based evaluation, introduce a datastore method that fetches orbit version by host UUID in a single indexed query (avoiding the extra HostLite lookup), and only run it when there is a realistic chance the desired schedule differs (e.g. when transitioning from fast->relaxed, or on a periodic re-check interval).

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


4. Poll reconcile non-atomic 🐞 Bug ☼ Reliability
Description
reconcileWindowsMDMPollSchedule enqueues the DMClient poll Replace command and then records
poll_schedule_relaxed in a separate datastore call, so if the second write fails after the enqueue
succeeds, the next management session will enqueue additional redundant poll Replace commands. This
violates the intended “enqueue once per intended change” behavior and can grow the command queue
unnecessarily under transient DB failures.
Code

server/service/microsoft_mdm.go[R2145-2154]

Evidence
The service enqueues the poll Replace command and then updates the intended schedule in two separate
datastore calls; the enqueue path is wrapped in its own transaction (withRetryTxx) while the
intended-state write is a separate standalone UPDATE, so the pair is not atomic and can partially
succeed.

server/service/microsoft_mdm.go[2135-2156]
server/datastore/mysql/microsoft_mdm.go[627-635]
server/datastore/mysql/microsoft_mdm.go[102-112]

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

## Issue description
`reconcileWindowsMDMPollSchedule` performs two separate DB operations: (1) enqueue poll Replace via `MDMWindowsInsertCommandForHosts`, then (2) `UPDATE mdm_windows_enrollments.poll_schedule_relaxed`. If (1) commits but (2) fails, `PollScheduleRelaxed` remains stale so subsequent sessions will keep enqueueing new poll Replace commands.
## Issue Context
The code comments state `poll_schedule_relaxed` is the intended schedule and should cause the Replace to be enqueued exactly once per intended change. That invariant currently depends on two non-atomic datastore calls.
## Fix Focus Areas
- server/service/microsoft_mdm.go[2145-2154]
- server/datastore/mysql/microsoft_mdm.go[102-112]
- server/datastore/mysql/microsoft_mdm.go[627-635]
## Suggested fix approach
1. Add a new datastore method that, in a single `withRetryTxx` transaction, does BOTH:
- Insert the poll schedule command into `windows_mdm_commands` and `windows_mdm_command_queue` for the *specific enrollment id* (no extra lookup needed since `device.ID` is the enrollment id).
- Update `mdm_windows_enrollments.poll_schedule_relaxed` for that enrollment id.
2. Call this new atomic datastore method from `reconcileWindowsMDMPollSchedule` instead of the two separate calls.
3. Keep the current ordering semantics (don’t update `poll_schedule_relaxed` unless the command enqueue succeeds) to ensure the server will retry on failures.

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


5. Poll state can drift ✓ Resolved 🐞 Bug ☼ Reliability
Description
getPollScheduleCommands updates mdm_windows_enrollments.poll_schedule_relaxed before the Replace
command is delivered/applied, and later treats that DB value as the applied state to decide whether
to send more Replace commands. If a restore-to-fast Replace is dropped/not applied, the DB flips to
false and the server stops retrying, potentially leaving a non-wake-capable host on the relaxed poll
interval and delaying Windows MDM command delivery.
Code

server/service/microsoft_mdm.go[R2073-2091]

Evidence
The server compares desiredRelaxed to device.PollScheduleRelaxed (DB-backed) to decide whether to
emit a Replace, and it persists poll_schedule_relaxed immediately when generating the Replace.
Since the enrollment reads and writes are purely DB-based, a delivery/apply failure can make the DB
state diverge from the device state with no built-in correction.

server/service/microsoft_mdm.go[2061-2092]
server/datastore/mysql/microsoft_mdm.go[67-109]
server/fleet/microsoft_mdm.go[856-879]

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

## Issue description
`getPollScheduleCommands` records `poll_schedule_relaxed` immediately when generating a Poll/Interval Replace, but the device has not yet applied/acknowledged that Replace. Since `PollScheduleRelaxed` is read from the DB and used to short-circuit future Replace emission, any delivery/apply failure can leave the DB permanently out of sync with the device.
This is especially problematic on the **restore** path (desiredRelaxed=false, stored=true): if the restore Replace is not applied but the DB is flipped to false, subsequent sessions will not resend the restore and the device may stay on a relaxed poll even though it is not wake-capable.
### Issue Context
- `PollScheduleRelaxed` is purely DB-derived (loaded in the enrollment SELECT), not device-reported.
- The current logic has no confirmation/reconciliation step before persisting the applied state.
### Fix Focus Areas
- server/service/microsoft_mdm.go[2061-2092]
- server/datastore/mysql/microsoft_mdm.go[67-109]
- server/fleet/microsoft_mdm.go[856-879]
### Suggested fix options
Choose one:
**Option A (minimal + safer restore):**
- Keep optimistic marking for `desiredRelaxed=true` (fast->relaxed), since failure leaves device on fast poll.
- For `desiredRelaxed=false` (relaxed->fast), do **not** flip the DB to false immediately. Keep it true so the restore Replace is re-sent on subsequent sessions until you add a stronger confirmation mechanism.
**Option B (true reconciliation):**
- Introduce a reconciliation mechanism that only updates `poll_schedule_relaxed` after observing device confirmation (e.g., by issuing an additional Get and updating DB based on the returned value, or by tracking/processing Status for the Replace target).
In either approach, update comments to reflect the asymmetry and the exact guarantees the system provides.

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



Advisory comments

6. Awaiting lookup extra work ✓ Resolved 🐞 Bug ➹ Performance
Description
GetMDMWindowsAwaitingConfigurationByHostUUID now delegates to GetMDMWindowsHostConfigState, which
also computes pending-command state via correlated EXISTS, even for callers that only need
awaiting_configuration. This weakens the interface’s stated “lightweight read” contract and adds
unnecessary DB work on non-orbit paths.
Code

server/datastore/mysql/microsoft_mdm.go[R201-207]

Evidence
The interface explicitly calls the awaiting lookup a lightweight orbit polling read, but the new
implementation executes the combined query that consults command queue/results. At least one caller
(setup-experience cancellation gate) uses the awaiting value only, so it now performs extra DB work
without using the additional state.

server/fleet/datastore.go[2161-2169]
server/datastore/mysql/microsoft_mdm.go[198-247]
server/service/setup_experience.go[268-320]

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

## Issue description
`GetMDMWindowsAwaitingConfigurationByHostUUID` is documented as a lightweight read, but it now calls `GetMDMWindowsHostConfigState`, which also checks the command queue/results tables. Callers that only need `awaiting_configuration` (e.g., setup-experience cancellation gating) now pay for extra work unrelated to their needs.
### Issue Context
This is not a correctness bug, but it increases coupling and makes it harder to reason about/optimize the “awaiting only” lookup independently.
### Fix Focus Areas
- Reintroduce a simple `SELECT awaiting_configuration ...` implementation for `GetMDMWindowsAwaitingConfigurationByHostUUID` (or update docs/contract if you intentionally want it to remain combined everywhere).
- server/datastore/mysql/microsoft_mdm.go[198-207]
- server/fleet/datastore.go[2161-2169]
- server/service/setup_experience.go[268-320]

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


Grey Divider

Qodo Logo

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 is a partial server-side step toward replacing Fleet's aggressive 1-minute Windows MDM polling (issue #43773). Instead of WNS push notifications, it introduces an in-band signal on the orbit/config polling path: when the server detects queued, unacknowledged Windows MDM commands for a host, it sets a new WindowsMDMSyncRequest notification, gated on a new CapabilityWindowsMDMSync capability. To avoid an extra DB round trip per check-in, the existing GetMDMWindowsAwaitingConfigurationByHostUUID lookup is folded into a new combined query GetMDMWindowsHostConfigState that also returns whether the host has pending commands.

Changes:

  • New datastore method GetMDMWindowsHostConfigState returning awaiting-configuration + has-pending-commands in one query; old method is reimplemented on top of it.
  • New OrbitConfigNotifications.WindowsMDMSyncRequest field and CapabilityWindowsMDMSync, with GetOrbitConfig setting the field when commands are queued and the client advertises the capability.
  • Datastore mock and tests (mysql + orbit service) updated to cover the new state and capability gating.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
server/fleet/capabilities.go Adds CapabilityWindowsMDMSync and advertises it from Windows fleetd.
server/fleet/orbit.go Adds WindowsMDMSyncRequest notification field.
server/fleet/microsoft_mdm.go Adds MDMWindowsHostConfigState type.
server/fleet/datastore.go Declares new GetMDMWindowsHostConfigState datastore method.
server/datastore/mysql/microsoft_mdm.go Implements combined awaiting-configuration + pending-commands query; old method delegates.
server/datastore/mysql/microsoft_mdm_test.go New integration test covering enrollment, pending command, ack, and awaiting-configuration.
server/service/orbit.go Switches orbit config path to combined query and sets new sync notification gated on capability.
server/service/orbit_test.go Updates existing tests and adds cases for capability gating and ESP-vs-sync precedence.
server/mock/datastore_mock.go Generated mock for the new datastore method.

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

Comment thread server/fleet/capabilities.go Outdated
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds a combined per-host Windows MDM config read (awaiting-configuration + denormalized has_pending_commands), persists a PollScheduleRelaxed flag on enrollments with a migration/backfill, recomputes has_pending_commands in command/response flows, reconciles DMClient poll schedule from management responses (enqueuing SyncML Replace when needed), introduces an Orbit receiver that triggers on‑demand Windows MDM syncs (with platform exec), and updates mocks and tests across datastore, service, orbit, and migrations.

Possibly related PRs

  • fleetdm/fleet#45331: Overlaps changes to Windows setup-experience cancellation flow and datastore lookups used in that gate.
  • fleetdm/fleet#45994: Also modifies setup-experience cancellation gating and awaiting-configuration handling tied to the same functions.
  • fleetdm/fleet#46268: Touches MDMWindows response-saving paths; both PRs modify MDMWindowsSaveResponse and related recompute/acknowledgement logic.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR changes implement on-demand Windows MDM sync infrastructure but do not include the full WNS push notification integration described in issue #43773, which requires Microsoft Store app registration and WNS cloud integration. Clarify whether this PR is an intermediate step toward WNS integration or a different approach; if intermediate, confirm the scope aligns with a planned follow-up PR for WNS.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed All code changes are focused on enabling on-demand Windows MDM sync through Orbit, poll schedule reconciliation, and infrastructure needed for pushing commands to devices when available.
Title check ✅ Passed The title 'flletd Windows MDM wake (push vs poll)' clearly describes the main objective: implementing an on-demand MDM sync mechanism for Windows fleetd as an alternative to aggressive polling.
Description check ✅ Passed The PR description provides a complete solution summary addressing the core problems (aggressive polling), implementation approach (sync capability with on-demand MDM delivery), key technical details (poll_schedule_relaxed flag, has_pending_commands state), backwards compatibility (older fleetd keeps 1-minute poll), and includes all required checklist items for the categories applicable to this change.

✏️ 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 victor/43773-fleetd-mdm-wake

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.

🧹 Nitpick comments (1)
server/datastore/mysql/microsoft_mdm_test.go (1)

1975-2031: ⚡ Quick win

Add a re-enrollment case for HasPendingCommands.

This only exercises a single enrollment. The risky regression here is a host with multiple mdm_windows_enrollments rows: if an older enrollment still has a queued command, GetMDMWindowsHostConfigState can incorrectly report HasPendingCommands=true for the current device and keep orbit sending spurious Windows MDM sync notifications. Please add a case that leaves a pending command on a superseded enrollment and asserts only the latest enrollment is considered.

🤖 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/microsoft_mdm_test.go` around lines 1975 - 2031, Add a
re-enrollment scenario to ensure GetMDMWindowsHostConfigState only considers the
latest enrollment: create a second enrollment for the same HostUUID (call
MDMWindowsInsertEnrolledDevice again with a new MDMDeviceID/MDMHardwareID but
same HostUUID), fetch both enrollment IDs via
mdmWindowsEnrollmentIDByHardwareID, insert a pending command tied to the older
enrollment_id directly (use ds.writer().ExecContext to INSERT into
windows_mdm_commands or the appropriate commands table with enrollment_id =
oldEnrollmentID), then call GetMDMWindowsHostConfigState(hostUUID) and assert
HasPendingCommands is false (only the most recent enrollment should be
considered); reference the test function testMDMWindowsGetHostConfigState and
methods MDMWindowsInsertEnrolledDevice, mdmWindowsEnrollmentIDByHardwareID,
GetMDMWindowsHostConfigState, and ds.writer() to locate where to add this case.
🤖 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/microsoft_mdm_test.go`:
- Around line 1975-2031: Add a re-enrollment scenario to ensure
GetMDMWindowsHostConfigState only considers the latest enrollment: create a
second enrollment for the same HostUUID (call MDMWindowsInsertEnrolledDevice
again with a new MDMDeviceID/MDMHardwareID but same HostUUID), fetch both
enrollment IDs via mdmWindowsEnrollmentIDByHardwareID, insert a pending command
tied to the older enrollment_id directly (use ds.writer().ExecContext to INSERT
into windows_mdm_commands or the appropriate commands table with enrollment_id =
oldEnrollmentID), then call GetMDMWindowsHostConfigState(hostUUID) and assert
HasPendingCommands is false (only the most recent enrollment should be
considered); reference the test function testMDMWindowsGetHostConfigState and
methods MDMWindowsInsertEnrolledDevice, mdmWindowsEnrollmentIDByHardwareID,
GetMDMWindowsHostConfigState, and ds.writer() to locate where to add this case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4523e48-9a2d-43ab-b11c-3ec6a6ba9232

📥 Commits

Reviewing files that changed from the base of the PR and between be56fd5 and 55c432e.

📒 Files selected for processing (9)
  • server/datastore/mysql/microsoft_mdm.go
  • server/datastore/mysql/microsoft_mdm_test.go
  • server/fleet/capabilities.go
  • server/fleet/datastore.go
  • server/fleet/microsoft_mdm.go
  • server/fleet/orbit.go
  • server/mock/datastore_mock.go
  • server/service/orbit.go
  • server/service/orbit_test.go

Comment thread server/service/orbit.go
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.08511% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.91%. Comparing base (9f5d31f) to head (a7c9c5d).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
server/datastore/mysql/microsoft_mdm.go 62.50% 18 Missing and 15 partials ⚠️
server/service/microsoft_mdm.go 72.41% 4 Missing and 4 partials ⚠️
...0_AddPollScheduleRelaxedToMDMWindowsEnrollments.go 72.72% 4 Missing and 2 partials ⚠️
orbit/pkg/update/notifications.go 82.60% 3 Missing and 1 partial ⚠️
orbit/cmd/orbit/orbit.go 0.00% 2 Missing ⚠️
orbit/pkg/update/execwinapi_stub.go 0.00% 2 Missing ⚠️
server/fleet/capabilities.go 0.00% 1 Missing and 1 partial ⚠️
server/service/orbit.go 86.66% 1 Missing and 1 partial ⚠️
server/service/setup_experience.go 80.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #46594    +/-   ##
========================================
  Coverage   66.91%   66.91%            
========================================
  Files        2833     2836     +3     
  Lines      224964   225185   +221     
  Branches    11620    11620            
========================================
+ Hits       150526   150676   +150     
- Misses      60781    60821    +40     
- Partials    13657    13688    +31     
Flag Coverage Δ
backend 68.62% <68.08%> (-0.01%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 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.

Relax the Windows MDM DMClient poll schedule in-session, gated on the host's
fleetd (orbit) version and reconciled against a persisted applied-state column.

- getPollScheduleCommands: desired-relaxed (orbit >= 1.56.0, via GetHostOrbitInfo)
  vs applied (mdm_windows_enrollments.poll_schedule_relaxed). Emits one Replace on
  .../Poll/IntervalForFirstSetOfRetries (60 relax / 1 restore) only when they differ.
- windowsMDMHostSupportsSync: missing/unlinked host or unknown version => not-capable
  (stay on the fast poll, the safe default).
- Migration 20260601204606 adds poll_schedule_relaxed TINYINT(1) NOT NULL DEFAULT 0.
- Datastore setter + enrolled-device SELECT + interface + regenerated mock.
- Tests: TestGetPollScheduleCommands, testMDMWindowsPollScheduleRelaxed, migration test.
@getvictor getvictor requested a review from Copilot June 1, 2026 21:19
@getvictor

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@getvictor

Copy link
Copy Markdown
Member Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1527c70

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

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Comment thread server/service/microsoft_mdm.go Outdated
Comment thread orbit/pkg/update/execwinapi_windows.go Outdated

@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: 5

🧹 Nitpick comments (1)
server/fleet/datastore.go (1)

2170-2173: ⚡ Quick win

Deprecate or narrow the older orbit-path getter.

GetMDMWindowsAwaitingConfigurationByHostUUID still advertises itself as the orbit config polling API, so adding this consolidated getter leaves two interface contracts for the same path. That makes it easy for a future caller to pick the older one-field method and reintroduce the extra datastore round trip this PR is trying to eliminate. Consider marking the older method deprecated or updating its doc to steer orbit callers here instead.

🤖 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/fleet/datastore.go` around lines 2170 - 2173, The older single-field
getter GetMDMWindowsAwaitingConfigurationByHostUUID is now redundant and risks
reintroducing extra datastore calls; update its declaration comment to mark it
deprecated and clearly steer callers to use the new consolidated
GetMDMWindowsHostConfigState(ctx, hostUUID string) method (mention the new
method by name in the comment), or if you prefer, add a deprecation comment
annotation above GetMDMWindowsAwaitingConfigurationByHostUUID and update any
public docs/comments to instruct orbit config polling callers to use
GetMDMWindowsHostConfigState instead so future developers choose the
consolidated API.
🤖 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 `@orbit/pkg/update/execwinapi_windows.go`:
- Around line 234-261: The function fleetMDMEnrollmentGUID currently treats
EnrollmentState==1 as active; update the logic in fleetMDMEnrollmentGUID to
consider EnrollmentState==4 (the typical active/working MDM state) or accept
both 1 and 4 to be safe: change the enrollmentStateActive constant or the
comparison in the loop that checks state (the block that reads "EnrollmentState"
and compares it to enrollmentStateActive) so it matches 4 (or checks state == 1
|| state == 4) when deciding an active Fleet MDM enrollment; keep the rest of
the registry reading/validation (ProviderID check and providerID ==
fleetMDMProviderID) unchanged.
- Around line 209-229: The comment and behavior in TriggerWindowsMDMSync that
call deviceenroller.exe with "/o <GUID> /c" and assert that avoiding "/c /z"
avoids Event 4603 lack provenance; update the comment above deviceEnroller or at
the top of TriggerWindowsMDMSync to cite concrete sources or tests (e.g.,
Microsoft docs/KB, blog posts, or internal repro ticket) that document the
deviceenroller.exe CLI flags and the Event 4603 behavior for specific Windows
builds, or explicitly state that the flags and the Event 4603 mitigation are
based on observed behavior and list the tested Windows versions and reproduction
steps; include links/IDs and the exact OS versions tested so reviewers can
verify the claim.

In `@server/datastore/mysql/microsoft_mdm_test.go`:
- Around line 2010-2066: Add a re-enrollment scenario to
testMDMWindowsGetHostConfigState: insert a second MDMWindowsEnrolledDevice for
the same HostUUID (distinct MDMDeviceID/MDMHardwareID) via
MDMWindowsInsertEnrolledDevice so there are two enrollment rows; use
mdmWindowsEnrollmentIDByHardwareID to fetch the enrollment IDs for both rows,
then insert a pending command and/or responses against the older (stale)
enrollment via MDMWindowsInsertCommandForHosts and direct writer ExecContext but
leave the newer enrollment with no pending commands and set awaiting
configuration on the newest enrollment with SetMDMWindowsAwaitingConfiguration;
finally call GetMDMWindowsHostConfigState and assert the combined state is taken
from the latest enrollment only (AwaitingConfiguration matches the new
enrollment and HasPendingCommands is false despite pending data on the stale
enrollment) to ensure queries are scoped to the most recent enrollment.

In
`@server/datastore/mysql/migrations/tables/20260601204606_AddPollScheduleRelaxedToMDMWindowsEnrollments.go`:
- Around line 23-24: The Down_20260601204606 migration is a no-op; implement it
to reverse the Up change by removing the added column so downgrades work: in
Down_20260601204606(tx *sql.Tx) execute an ALTER TABLE mdm_windows_enrollments
DROP COLUMN poll_schedule_relaxed (using tx.Exec), handle and return any error
from Exec, and ensure the function returns nil only on success; this mirrors the
Up that added poll_schedule_relaxed and restores the schema to its prior state.

In `@server/service/microsoft_mdm.go`:
- Around line 2087-2089: The code is optimistically persisting
PollScheduleRelaxed via svc.ds.SetMDMWindowsEnrollmentPollScheduleRelaxed when
emitting the SyncML Replace, which can mark the state reconciled before the
device actually applies it; change the flow so that you only update the stored
PollScheduleRelaxed after the device acknowledges the command (i.e., on the
confirmation/ack handler), or instead enqueue the Replace through the existing
retryable command pipeline and derive/update the DB state when that retryable
command reports success. Locate usages of
SetMDMWindowsEnrollmentPollScheduleRelaxed, the Replace emission in the SyncML
response, and the device acknowledgment handler or retryable command codepath
and move the DB write there (or wire the Replace into the retryable command
queue) so failed/dropped responses will be retried and the DB only flips when
the device confirms the change.

---

Nitpick comments:
In `@server/fleet/datastore.go`:
- Around line 2170-2173: The older single-field getter
GetMDMWindowsAwaitingConfigurationByHostUUID is now redundant and risks
reintroducing extra datastore calls; update its declaration comment to mark it
deprecated and clearly steer callers to use the new consolidated
GetMDMWindowsHostConfigState(ctx, hostUUID string) method (mention the new
method by name in the comment), or if you prefer, add a deprecation comment
annotation above GetMDMWindowsAwaitingConfigurationByHostUUID and update any
public docs/comments to instruct orbit config polling callers to use
GetMDMWindowsHostConfigState instead so future developers choose the
consolidated API.
🪄 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: 5b5df59b-8b7b-4685-a25b-6f2f4568f13e

📥 Commits

Reviewing files that changed from the base of the PR and between be56fd5 and 1527c70.

📒 Files selected for processing (19)
  • orbit/cmd/orbit/orbit.go
  • orbit/pkg/update/execwinapi_stub.go
  • orbit/pkg/update/execwinapi_windows.go
  • orbit/pkg/update/notifications.go
  • orbit/pkg/update/notifications_test.go
  • server/datastore/mysql/microsoft_mdm.go
  • server/datastore/mysql/microsoft_mdm_test.go
  • server/datastore/mysql/migrations/tables/20260601204606_AddPollScheduleRelaxedToMDMWindowsEnrollments.go
  • server/datastore/mysql/migrations/tables/20260601204606_AddPollScheduleRelaxedToMDMWindowsEnrollments_test.go
  • server/datastore/mysql/schema.sql
  • server/fleet/capabilities.go
  • server/fleet/datastore.go
  • server/fleet/microsoft_mdm.go
  • server/fleet/orbit.go
  • server/mock/datastore_mock.go
  • server/service/microsoft_mdm.go
  • server/service/microsoft_mdm_test.go
  • server/service/orbit.go
  • server/service/orbit_test.go

Comment thread orbit/pkg/update/execwinapi_windows.go
Comment thread orbit/pkg/update/execwinapi_windows.go
Comment thread server/datastore/mysql/microsoft_mdm_test.go
Comment thread server/service/microsoft_mdm.go Outdated
getvictor added 2 commits June 2, 2026 14:30
…dm-wake

# Conflicts:
#	server/datastore/mysql/schema.sql
#	server/service/microsoft_mdm_test.go
@getvictor

Copy link
Copy Markdown
Member Author

@claude review once

Comment thread server/datastore/mysql/microsoft_mdm.go
@getvictor getvictor marked this pull request as ready for review June 3, 2026 18:20
@getvictor getvictor requested a review from a team as a code owner June 3, 2026 18:20

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@getvictor getvictor changed the title fleetd Windows MDM wake fleetd Windows MDM wake (push vs poll) Jun 3, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: aggregate-result

Failed stage: Check for failures [❌]

Failed test name: vuln-mysql8.0.44

Failure summary:

The action failed because the status aggregation step detected a failing test job:
- The file
./vuln-mysql8.0.44-status/status contained fail, so the script marked vuln-mysql8.0.44 as failed.
-
The workflow then exited with code 1 at the final check (❌ One or more test jobs failed:
vuln-mysql8.0.44), causing the action to fail.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

119:  Artifact download completed successfully.
120:  Extracting artifact entry: /home/runner/work/fleet/fleet/mysql-mysql8.0.44-status/status
121:  Artifact download completed successfully.
122:  Extracting artifact entry: /home/runner/work/fleet/fleet/main-mysql8.0.44-status/status
123:  Artifact download completed successfully.
124:  Extracting artifact entry: /home/runner/work/fleet/fleet/integration-core-mysql8.0.44-status/status
125:  Artifact download completed successfully.
126:  Extracting artifact entry: /home/runner/work/fleet/fleet/fast-status/status
127:  Extracting artifact entry: /home/runner/work/fleet/fleet/vuln-mysql8.0.44-status/status
128:  Artifact download completed successfully.
129:  Artifact download completed successfully.
130:  Extracting artifact entry: /home/runner/work/fleet/fleet/scripts-status/status
131:  Artifact download completed successfully.
132:  Total of 10 artifact(s) downloaded
133:  Download artifact has finished successfully
134:  ##[group]Run failed_tests=""
135:  �[36;1mfailed_tests=""�[0m
136:  �[36;1mstatus_count=0�[0m
137:  �[36;1m# Find all status files (they are in directories like 'fleetctl-mysql8.0.44-status/status')�[0m
138:  �[36;1mfor status_file in $(find ./ -type f -name 'status'); do�[0m
139:  �[36;1m  status_count=$((status_count + 1))�[0m
140:  �[36;1m  # Extract test name from parent directory (e.g., 'fleetctl-mysql8.0.44-status')�[0m
141:  �[36;1m  test_dir=$(basename $(dirname "$status_file"))�[0m
142:  �[36;1m  # Remove '-status' suffix to get the test name�[0m
143:  �[36;1m  test_name="${test_dir%-status}"�[0m
144:  �[36;1m  status_content=$(cat "$status_file")�[0m
145:  �[36;1m  echo "Processing: $status_file (Test: $test_name) with status content: $status_content"�[0m
146:  �[36;1m  if grep -q "fail" "$status_file"; then�[0m
147:  �[36;1m    echo "  ❌ Test failed: $test_name"�[0m
148:  �[36;1m    failed_tests="${failed_tests}${test_name}, "�[0m
149:  �[36;1m  else�[0m
150:  �[36;1m    echo "  ✅ Test passed: $test_name"�[0m
151:  �[36;1m  fi�[0m
152:  �[36;1mdone�[0m
153:  �[36;1mif [[ $status_count -eq 0 ]]; then�[0m
154:  �[36;1m  echo "❌ ERROR: No status files found! This indicates a workflow issue."�[0m
155:  �[36;1m  exit 1�[0m
156:  �[36;1mfi�[0m
157:  �[36;1mif [[ -n "$failed_tests" ]]; then�[0m
158:  �[36;1m  echo "❌ One or more test jobs failed: ${failed_tests%, }"�[0m
159:  �[36;1m  exit 1�[0m
160:  �[36;1mfi�[0m
161:  �[36;1mecho "✅ All test jobs succeeded."�[0m
162:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
163:  ##[endgroup]
164:  Processing: ./integration-core-mysql8.0.44-status/status (Test: integration-core-mysql8.0.44) with status content: success
165:  ✅ Test passed: integration-core-mysql8.0.44
166:  Processing: ./vuln-mysql8.0.44-status/status (Test: vuln-mysql8.0.44) with status content: fail
167:  ❌ Test failed: vuln-mysql8.0.44
168:  Processing: ./service-mysql8.0.44-status/status (Test: service-mysql8.0.44) with status content: success
169:  ✅ Test passed: service-mysql8.0.44
170:  Processing: ./main-mysql8.0.44-status/status (Test: main-mysql8.0.44) with status content: success
171:  ✅ Test passed: main-mysql8.0.44
172:  Processing: ./fleetctl-mysql8.0.44-status/status (Test: fleetctl-mysql8.0.44) with status content: success
173:  ✅ Test passed: fleetctl-mysql8.0.44
174:  Processing: ./integration-enterprise-mysql8.0.44-status/status (Test: integration-enterprise-mysql8.0.44) with status content: success
175:  ✅ Test passed: integration-enterprise-mysql8.0.44
176:  Processing: ./fast-status/status (Test: fast) with status content: success
177:  ✅ Test passed: fast
178:  Processing: ./mysql-mysql8.0.44-status/status (Test: mysql-mysql8.0.44) with status content: success
179:  ✅ Test passed: mysql-mysql8.0.44
180:  Processing: ./integration-mdm-mysql8.0.44-status/status (Test: integration-mdm-mysql8.0.44) with status content: success
181:  ✅ Test passed: integration-mdm-mysql8.0.44
182:  Processing: ./scripts-status/status (Test: scripts) with status content: success
183:  ✅ Test passed: scripts
184:  ❌ One or more test jobs failed: vuln-mysql8.0.44
185:  ##[error]Process completed with exit code 1.
186:  Post job cleanup.

@ksykulev ksykulev self-assigned this Jun 3, 2026

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

Now that these commands are through orbit they will get logged. The GET /api/v1/fleet/mdm/commands will return these? Do we want to filter them or do we not care?

HasPendingCommands bool `db:"has_pending_commands"`
FleetdSyncCapable bool `db:"fleetd_sync_capable"`
}
if err := sqlx.GetContext(ctx, ds.reader(ctx), &row, stmt, hostUUID); err != nil {

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 ok that we read from the replica here? has_pending_commands is written on the primary right?

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.

Yes, we are talking about 30 second queries here from orbit, so we are not on a tight budget. Also, this read is on a hot path, so should use replica here.

@getvictor

Copy link
Copy Markdown
Member Author

Now that these commands are through orbit they will get logged. The GET /api/v1/fleet/mdm/commands will return these? Do we want to filter them or do we not care?

@ksykulev The MDM commands are not through orbit. orbit tells Windows to sync with MDM, so Windows still initiates the MDM session and pulls down any commands (as before). We are adding the new command to change the Poll frequency, which I expect to only happen once in the lifetime of the host, and that will show up in mdm/commands. Didn't seem worth it to filter/hide it.

@getvictor getvictor requested a review from ksykulev June 3, 2026 20:57
@getvictor getvictor linked an issue Jun 3, 2026 that may be closed by this pull request
2 tasks

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

I mis-understood the mechanism. This looks good then!

@getvictor getvictor merged commit e20cedc into main Jun 3, 2026
53 of 55 checks passed
@getvictor getvictor deleted the victor/43773-fleetd-mdm-wake branch June 3, 2026 22:38
@coderabbitai coderabbitai Bot mentioned this pull request Jun 5, 2026
5 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.

Windows MDM push (device wake) via fleetd orbit: Windows MDM push (device wake) via fleetd CSP setting continuously reapplying

3 participants