Skip to content

Removed duplicate FlippingPoliciesForHost DB calls#42845

Merged
getvictor merged 4 commits intomainfrom
victor/42836-flipping-policies
Apr 6, 2026
Merged

Removed duplicate FlippingPoliciesForHost DB calls#42845
getvictor merged 4 commits intomainfrom
victor/42836-flipping-policies

Conversation

@getvictor
Copy link
Copy Markdown
Member

@getvictor getvictor commented Apr 1, 2026

Related issue: Resolves #42836

This is another hot path optimization.

Before

When a host submits policy results via SubmitDistributedQueryResults, the system needed to determine which policies "flipped" (changed from passing to failing or vice versa). Each consumer computed this independently:

SubmitDistributedQueryResults(policyResults)
  |
  +-- processScriptsForNewlyFailingPolicies
  |     filter to failing policies with scripts
  |     BUILD SUBSET of results
  |     CALL FlippingPoliciesForHost(subset)          <-- DB query #1
  |     convert result to set, filter, queue scripts
  |
  +-- processSoftwareForNewlyFailingPolicies
  |     filter to failing policies with installers
  |     BUILD SUBSET of results
  |     CALL FlippingPoliciesForHost(subset)          <-- DB query #2
  |     convert result to set, filter, queue installs
  |
  +-- processVPPForNewlyFailingPolicies
  |     filter to failing policies with VPP apps
  |     BUILD SUBSET of results
  |     CALL FlippingPoliciesForHost(subset)          <-- DB query #3
  |     convert result to set, filter, queue VPP
  |
  +-- webhook filtering
  |     filter to webhook-enabled policies
  |     CALL FlippingPoliciesForHost(subset)          <-- DB query #4
  |     register flipped policies in Redis
  |
  +-- RecordPolicyQueryExecutions
        CALL FlippingPoliciesForHost(all results)     <-- DB query #5
        reset attempt counters for newly passing
        INSERT/UPDATE policy_membership

Each FlippingPoliciesForHost call runs SELECT policy_id, passes FROM policy_membership WHERE host_id = ? AND policy_id IN (?). All 5 queries hit the same table for the same host before policy_membership is updated, so they all see identical state.

Each consumer also built intermediate maps to narrow down to its subset before calling FlippingPoliciesForHost, then converted the result into yet another set for filtering. This meant 3-4 temporary maps per consumer.

After

SubmitDistributedQueryResults(policyResults)
  |
  CALL FlippingPoliciesForHost(all results)           <-- single DB query
  build newFailingSet, normalize newPassing
  |
  +-- processScriptsForNewlyFailingPolicies
  |     filter to failing policies with scripts
  |     CHECK newFailingSet (in-memory map lookup)
  |     queue scripts
  |
  +-- processSoftwareForNewlyFailingPolicies
  |     filter to failing policies with installers
  |     CHECK newFailingSet (in-memory map lookup)
  |     queue installs
  |
  +-- processVPPForNewlyFailingPolicies
  |     filter to failing policies with VPP apps
  |     CHECK newFailingSet (in-memory map lookup)
  |     queue VPP
  |
  +-- webhook filtering
  |     filter to webhook-enabled policies
  |     FILTER newFailing/newPassing by policy IDs (in-memory)
  |     register flipped policies in Redis
  |
  +-- RecordPolicyQueryExecutions
        USE pre-computed newPassing (skip DB query)
        reset attempt counters for newly passing
        INSERT/UPDATE policy_membership

The intermediate subset maps and per-consumer set conversions are removed. Each process function goes directly from "policies with associated automation" to "is this policy in newFailingSet?" in a single map lookup.

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.

Testing

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

Summary by CodeRabbit

  • Performance Improvements
    • Reduced redundant database queries during policy result submissions by computing flipping policies once per host check-in instead of multiple times.

@getvictor
Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 reduces redundant MySQL queries during host policy result submission by deduplicating repeated FlippingPoliciesForHost calls within SubmitDistributedQueryResults, aiming to lower DB load during large-fleet check-ins.

Changes:

  • Precompute flipping policy sets once per request and reuse them for scripts, software installers, VPP, and webhook handling.
  • Extend RecordPolicyQueryExecutions to optionally accept precomputed “newly passing” policy IDs to avoid an additional flipping query.
  • Update mocks/tests and add a changelog entry to reflect the optimization.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
server/service/osquery.go Precomputes flipping results once; threads newFailingSet through policy automation consumers; replaces webhook flipping query with in-process filtering.
server/service/osquery_test.go Updates mocked RecordPolicyQueryExecutionsFunc signature to include the new parameter.
server/service/orbit_test.go Updates RecordPolicyQueryExecutions calls to include the new parameter.
server/service/integration_enterprise_test.go Updates RecordPolicyQueryExecutions calls to include the new parameter across integration tests.
server/service/integration_core_test.go Updates RecordPolicyQueryExecutions calls to include the new parameter across integration tests.
server/service/async/async_test.go Updates mocked RecordPolicyQueryExecutionsFunc signature to include the new parameter.
server/service/async/async_policy.go Threads new parameter through async task wrapper to datastore implementation.
server/service/async/async_policy_test.go Updates RecordPolicyQueryExecutions calls to include the new parameter.
server/mock/datastore_mock.go Updates datastore mock interface + forwarding method to accept the new parameter.
server/fleet/datastore.go Updates datastore interface contract and documents the new optional argument.
server/datastore/mysql/policies.go Uses provided “newly passing” IDs to skip internal FlippingPoliciesForHost call.
server/datastore/mysql/policies_test.go Updates MySQL datastore tests for the new RecordPolicyQueryExecutions signature.
server/datastore/mysql/labels_test.go Updates calls to RecordPolicyQueryExecutions to include the new parameter.
server/datastore/mysql/hosts_test.go Updates calls to RecordPolicyQueryExecutions to include the new parameter.
server/datastore/mysql/conditional_access_bypass_test.go Updates calls to RecordPolicyQueryExecutions to include the new parameter.
changes/42836-deduplicate-flipping-policies-queries Adds a changelog note for the DB query reduction.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Walkthrough

This PR deduplicates redundant database queries during policy result submission. When a host submits policy results via SubmitDistributedQueryResults, the FlippingPoliciesForHost query was previously called up to five times with the same or overlapping policy ID sets. The PR computes flipping policies once and passes the precomputed results through the processing pipeline. The RecordPolicyQueryExecutions method signature was extended with an optional newlyPassingPolicyIDs parameter; when provided, this precomputed set is used directly instead of querying the database. All call sites across tests and implementations were updated accordingly.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: removing duplicate FlippingPoliciesForHost database calls, which is the primary optimization detailed in the PR summary and linked issue.
Linked Issues check ✅ Passed The PR implementation achieves the core objective: FlippingPoliciesForHost is called once per host check-in instead of up to five times, with precomputed results passed to all consumers, validating the proposed fix from issue #42836.
Out of Scope Changes check ✅ Passed All changes align with the deduplication objective: method signature extensions, test updates, and refactored policy processing logic to eliminate redundant database queries. No unrelated changes detected.
Description check ✅ Passed The PR description includes a clear Related issue reference (#42836), detailed Before/After explanation of the optimization, and addresses most checklist items.

✏️ 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/42836-flipping-policies

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
server/service/osquery_test.go (1)

3352-3354: Consider asserting newlyPassingPolicyIDs in these mocks.

The new parameter is wired in, but not validated. Adding assertions here would lock in the new precomputed-flips contract and catch propagation regressions early.

Also applies to: 3663-3665

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/service/osquery_test.go` around lines 3352 - 3354, The mock for
ds.RecordPolicyQueryExecutionsFunc currently accepts the newlyPassingPolicyIDs
parameter but doesn't validate it; update the mock bodies (the one in the
osquery test and the other instance around the second occurrence) to assert the
received newlyPassingPolicyIDs equals the test's expected value (e.g., compare
to an expectedNewlyPassingPolicyIDs slice or check length/contents) using the
existing test assertion helpers (require/require.Equal/assert.ElementsMatch,
etc.), so the precomputed-flips contract is enforced and regressions are caught.
server/datastore/mysql/policies_test.go (1)

463-467: Add one explicit test for non-nil newlyPassingPolicyIDs.

This file now validates the new signature shape, but only with nil. A focused case with a populated newlyPassingPolicyIDs would better lock in the new execution path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/datastore/mysql/policies_test.go` around lines 463 - 467, The test
suite never exercises a non-nil newlyPassingPolicyIDs map; add one explicit test
call to RecordPolicyQueryExecutions that passes a non-nil map (e.g.,
map[uint]*bool{p.ID: ptr.Bool(true)}) for newlyPassingPolicyIDs and assert
require.NoError on the call so the new execution path is covered; place this
call alongside the existing RecordPolicyQueryExecutions calls in
policies_test.go and reference RecordPolicyQueryExecutions,
newlyPassingPolicyIDs and p.ID to locate where to add it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/datastore/mysql/policies_test.go`:
- Around line 463-467: The test suite never exercises a non-nil
newlyPassingPolicyIDs map; add one explicit test call to
RecordPolicyQueryExecutions that passes a non-nil map (e.g.,
map[uint]*bool{p.ID: ptr.Bool(true)}) for newlyPassingPolicyIDs and assert
require.NoError on the call so the new execution path is covered; place this
call alongside the existing RecordPolicyQueryExecutions calls in
policies_test.go and reference RecordPolicyQueryExecutions,
newlyPassingPolicyIDs and p.ID to locate where to add it.

In `@server/service/osquery_test.go`:
- Around line 3352-3354: The mock for ds.RecordPolicyQueryExecutionsFunc
currently accepts the newlyPassingPolicyIDs parameter but doesn't validate it;
update the mock bodies (the one in the osquery test and the other instance
around the second occurrence) to assert the received newlyPassingPolicyIDs
equals the test's expected value (e.g., compare to an
expectedNewlyPassingPolicyIDs slice or check length/contents) using the existing
test assertion helpers (require/require.Equal/assert.ElementsMatch, etc.), so
the precomputed-flips contract is enforced and regressions are caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7daaf6d2-6f47-4ef4-a942-add0937a0257

📥 Commits

Reviewing files that changed from the base of the PR and between 945b7e6 and d4c917c.

📒 Files selected for processing (16)
  • changes/42836-deduplicate-flipping-policies-queries
  • server/datastore/mysql/conditional_access_bypass_test.go
  • server/datastore/mysql/hosts_test.go
  • server/datastore/mysql/labels_test.go
  • server/datastore/mysql/policies.go
  • server/datastore/mysql/policies_test.go
  • server/fleet/datastore.go
  • server/mock/datastore_mock.go
  • server/service/async/async_policy.go
  • server/service/async/async_policy_test.go
  • server/service/async/async_test.go
  • server/service/integration_core_test.go
  • server/service/integration_enterprise_test.go
  • server/service/orbit_test.go
  • server/service/osquery.go
  • server/service/osquery_test.go

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 76.19048% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.84%. Comparing base (7329c89) to head (49762bb).
⚠️ Report is 131 commits behind head on main.

Files with missing lines Patch % Lines
server/service/osquery.go 73.33% 3 Missing and 5 partials ⚠️
server/datastore/mysql/policies.go 80.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #42845      +/-   ##
==========================================
+ Coverage   66.81%   66.84%   +0.02%     
==========================================
  Files        2541     2578      +37     
  Lines      203971   206826    +2855     
  Branches     9278     9278              
==========================================
+ Hits       136282   138245    +1963     
- Misses      55343    56017     +674     
- Partials    12346    12564     +218     
Flag Coverage Δ
backend 68.61% <76.19%> (+<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.

@getvictor getvictor marked this pull request as ready for review April 2, 2026 16:40
@getvictor getvictor requested a review from a team as a code owner April 2, 2026 16:40
Copy link
Copy Markdown

@claude claude bot left a comment

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.

Copy link
Copy Markdown
Member

@lucasmrod lucasmrod left a comment

Choose a reason for hiding this comment

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

LGTM! Left some questions.

We should add a test with non-nil newlyPassing on RecordPolicyQueryExecutions in the mysql package.

@@ -0,0 +1 @@
- Reduced redundant database queries during policy result submission by computing flipping policies once per host check-in instead of up to five times.
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.

For most common scenario it won't be 5 times, a policy usually has one purpose, so the optimization will actually be from 2 to 1 time (any of the automations + RecordPolicyQueryExecutions), right? (Which is actually a good thing anyways.)

} else if hostWithoutPolicies {
// RecordPolicyQueryExecutions called with results=nil will still update the host's policy_updated_at column.
if err := svc.task.RecordPolicyQueryExecutions(ctx, host, nil, svc.clock.Now(), ac.ServerSettings.DeferredSaveHost); err != nil {
if err := svc.task.RecordPolicyQueryExecutions(ctx, host, nil, svc.clock.Now(), ac.ServerSettings.DeferredSaveHost, nil); err != nil {
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.

Why nil here instead of []byte{}?

@getvictor getvictor merged commit 8af94af into main Apr 6, 2026
51 checks passed
@getvictor getvictor deleted the victor/42836-flipping-policies branch April 6, 2026 15:11
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.

Deduplicate FlippingPoliciesForHost queries during policy result submission

3 participants