Skip to content

Batch extension label-membership checks in GetOrbitConfig#49154

Open
sharon-fdm wants to merge 2 commits into
mainfrom
fix-45320-batch-label-membership
Open

Batch extension label-membership checks in GetOrbitConfig#49154
sharon-fdm wants to merge 2 commits into
mainfrom
fix-45320-batch-label-membership

Conversation

@sharon-fdm

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

Copy link
Copy Markdown
Collaborator

Related issue: Resolves #45320

Checklist for submitter

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

Summary

filterExtensionsForHost (called on every Orbit config fetch, ~30s per host) had an N+1 query pattern: it called HostMemberOfAllLabels once per extension in a loop, issuing a separate DB query for each.

This PR replaces the N queries with a single batch query via a new HostMembershipForLabels datastore method that returns which labels (from a given list) the host belongs to. Extension filtering then happens in-memory.

Changes

  • New datastore method HostMembershipForLabels(ctx, hostID, labelNames) -> map[string]bool -- single SELECT l.name FROM labels l JOIN label_membership query
  • Updated filterExtensionsForHost in server/service/orbit.go -- collects all unique label names across extensions, calls the new method once, filters in-memory
  • No API, UI, CLI, agent, or schema changes -- purely server-side internal optimization. Full backward compatibility: old agents work with new servers and vice versa (no protocol change).

Benchmark

Ran a local end-to-end benchmark against the live POST /api/fleet/orbit/config endpoint to measure the real-world impact.

Setup:

  • MacBook (Fleet server + Docker MySQL 8.0 + Redis, all localhost)
  • 50 enrolled Orbit hosts (darwin), 5 label-scoped extensions, all hosts members of all 5 labels
  • 500 requests at concurrency 10, cycling through all 50 orbit_node_keys
  • Built Fleet binary from main (before) and this PR branch (after), same database and test data

Results:

Metric Before (main) After (this PR) Improvement
Avg latency 25.33 ms 17.46 ms -31%, 1.45x faster
P50 latency 24.55 ms 16.52 ms -33%, 1.49x faster
P95 latency 34.42 ms 27.53 ms -20%, 1.25x faster
Throughput 390.6 req/s 564.2 req/s +44%

Extrapolation to 100,000 hosts

At 100k hosts with a 30-second check-in interval (3,333 req/s steady state):

Metric Before After
Server host capacity (measured MacBook) 11,718 16,926 (+44%)
Label-check DB queries/sec 16,665 (5/req) 3,333 (1/req)
DB queries eliminated 13,332/sec (80% reduction)

The improvement scales linearly with extension count:

Extensions DB queries eliminated/sec Reduction
5 13,332 80%
10 29,997 90%
15 46,662 93%
20 63,327 95%

Note: These are conservative localhost numbers. In production, where each DB round-trip includes real network latency, the per-request latency improvement would be more pronounced because each eliminated query saves a network hop.

Testing

Automated

  • New testHostMembershipForLabels MySQL integration test covering: empty input, full membership, partial membership, nonexistent labels, nonexistent host, host with no memberships
  • Existing testHostMemberOfAllLabels unchanged and unaffected

Manual QA

  1. Fleet Premium instance with 2+ Orbit-enrolled hosts
  2. Configure 3+ osquery extensions with different label scoping
  3. Verify each host receives only the extensions whose label requirements it meets
  4. Verify extensions with no label scoping are included for all hosts

Summary by CodeRabbit

  • Performance
    • Improved Orbit configuration loading by reducing the number of database checks required for extension label filtering.
    • Extension availability and filtering behavior remains unchanged, with more efficient processing when multiple extensions use labels.

Replace N+1 per-extension HostMemberOfAllLabels calls in
filterExtensionsForHost with a single HostMembershipForLabels query
that fetches all label memberships at once, then filters in memory.
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.95%. Comparing base (6d21671) to head (46f24bc).
⚠️ Report is 52 commits behind head on main.

Files with missing lines Patch % Lines
server/service/orbit.go 76.19% 3 Missing and 2 partials ⚠️
server/datastore/mysql/labels.go 77.77% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #49154      +/-   ##
==========================================
- Coverage   68.10%   67.95%   -0.16%     
==========================================
  Files        3732     3745      +13     
  Lines      235790   238286    +2496     
  Branches    12517    12517              
==========================================
+ Hits       160596   161935    +1339     
- Misses      60772    61699     +927     
- Partials    14422    14652     +230     
Flag Coverage Δ
backend 69.52% <76.92%> (-0.20%) ⬇️

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.

Comment thread server/fleet/datastore.go Outdated

// HostMembershipForLabels returns the set of label names (from the provided list) that the host is a member of.
// Labels that do not exist are not included in the result.
HostMembershipForLabels(ctx context.Context, hostID uint, labelNames []string) (map[string]bool, error)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Why a new method instead of modifying HostMemberOfAllLabels? Adding a new method means zero risk to other callers. The old method stays untouched, and filterExtensionsForHost switches to the new one.

- setboolcheck: HostMembershipForLabels return type changed from
  map[string]bool to map[string]struct{} (Fleet convention for sets)
- staticcheck: replaced ptr.String/ptr.Bool with new() in test
Comment thread server/fleet/datastore.go

// HostMembershipForLabels returns the set of label names (from the provided list) that the host is a member of.
// Labels that do not exist are not included in the result.
HostMembershipForLabels(ctx context.Context, hostID uint, labelNames []string) (map[string]struct{}, error)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Why a new method instead of modifying HostMemberOfAllLabels? Adding a new method means zero risk to other callers. The old method stays untouched, and filterExtensionsForHost switches to the new one.

@sharon-fdm sharon-fdm marked this pull request as ready for review July 10, 2026 20:27
@sharon-fdm sharon-fdm requested a review from a team as a code owner July 10, 2026 20:27
Copilot AI review requested due to automatic review settings July 10, 2026 20:27

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.

Warning

  • Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.

Pull request overview

This PR optimizes Orbit config generation by removing an N+1 label-membership query pattern in filterExtensionsForHost (called during GetOrbitConfig) and replacing it with a single batched datastore query for a host’s membership across all labels referenced by configured extensions.

Changes:

  • Added a new datastore API HostMembershipForLabels to fetch a host’s membership across a provided label list in one query.
  • Updated filterExtensionsForHost to batch label lookup and filter extensions in-memory.
  • Added a MySQL integration test for the new datastore method.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
server/service/orbit.go Replaces per-extension membership queries with a single batched membership lookup and in-memory filtering.
server/fleet/datastore.go Adds HostMembershipForLabels to the datastore interface.
server/mock/datastore_mock.go Extends the datastore mock to support HostMembershipForLabels.
server/datastore/mysql/labels.go Implements HostMembershipForLabels with a single SELECT ... IN (?) query.
server/datastore/mysql/labels_test.go Adds integration test coverage for HostMembershipForLabels.
changes/batch-extension-label-check Changes entry for the user-visible optimization (content excluded from review).
Files excluded by content exclusion policy (1)
  • changes/batch-extension-label-check

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

Comment on lines +1768 to +1772
result := make(map[string]struct{}, len(names))
for _, n := range names {
result[n] = struct{}{}
}
return result, nil
Comment on lines +2199 to +2204
{
name: "nonexistent labels are not included",
hostID: h1.ID,
labelNames: []string{allHostsLabel.Name, "nonexistent-label"},
expectedResult: map[string]struct{}{allHostsLabel.Name: {}},
},
@coderabbitai

coderabbitai Bot commented Jul 10, 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: 292e0eb5-bada-4120-8b94-8076ca628add

📥 Commits

Reviewing files that changed from the base of the PR and between 15118e4 and 46f24bc.

📒 Files selected for processing (6)
  • changes/batch-extension-label-check
  • server/datastore/mysql/labels.go
  • server/datastore/mysql/labels_test.go
  • server/fleet/datastore.go
  • server/mock/datastore_mock.go
  • server/service/orbit.go

Walkthrough

Adds a HostMembershipForLabels datastore method with MySQL and mock implementations plus integration tests. Updates Orbit extension filtering to collect unique label names, perform one host membership query, and filter extensions from the returned membership map. Adds a change note documenting the batched lookup.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The batch query and integration test are present, but the requested filterExtensionsForHost unit test coverage is not shown. Add unit tests for filterExtensionsForHost covering no extensions, platform-only filtering, and mixed membership results to satisfy #45320.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: batching extension label-membership checks in GetOrbitConfig.
Description check ✅ Passed The description includes the related issue, checklist items, a summary, testing details, and manual QA steps.
Out of Scope Changes check ✅ Passed The changes stay focused on batching label-membership checks and the supporting datastore/test updates, with no unrelated API or schema work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix-45320-batch-label-membership

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.

@nulmete nulmete self-assigned this Jul 13, 2026
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.

Batch extension label-membership checks in GetOrbitConfig to eliminate N+1 queries

3 participants