Skip to content

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

Description

@sharon-fdm

Goal

User story
As a Fleet engineer,
I want filterExtensionsForHost to check all extension label requirements in a single DB query,
so that the Orbit config endpoint does not issue N separate queries per host check-in (one per extension).

Context

Parent story: #35467

GetOrbitConfig is called every ~30 seconds by every Orbit-enrolled host. Part of that code path is filterExtensionsForHost (server/service/orbit.go:960), which decides which osquery extensions apply to a given host based on label membership (premium only).

Today this function loops through each extension and calls ds.HostMemberOfAllLabels(ctx, host.ID, extensionInfo.Labels) once per extension. Each call executes a separate SQL query joining labels and label_membership.

This is an N+1 pattern: for E label-scoped extensions, there are E round-trips to MySQL on every Orbit config fetch, for every host.

Impact estimate (10,000 hosts, 3 label-scoped extensions)

Metric Current After fix
DB queries per host per Orbit check-in (label filtering) 3 1
Steady-state label-filtering QPS ~1,000 ~333

At larger fleet sizes or more extensions, the savings grow linearly.

Current code (simplified)

// server/service/orbit.go:974-986
for extensionName, extensionInfo := range extensionsInfo {
    hostIsMemberOfAllLabels, err := svc.ds.HostMemberOfAllLabels(ctx, host.ID, extensionInfo.Labels)
    // ... filter based on result
}

Changes

Product

  • UI changes: None
  • CLI (fleetctl) usage changes: None
  • YAML changes: None
  • REST API changes: None
  • Fleet's agent (fleetd) changes: None

Engineering

  • Add a new Datastore method HostMemberOfAllLabelSets(ctx, hostID, labelSets map[string][]string) (map[string]bool, error) that checks membership for multiple label sets in a single query
    • Interface: server/fleet/datastore.go
    • MySQL impl: server/datastore/mysql/labels.go
    • Mock: regenerate with make generate-mock
  • Update filterExtensionsForHost in server/service/orbit.go to collect all extensions' label requirements, call the new batch method once, then filter based on the results
  • Add MySQL integration test for HostMemberOfAllLabelSets (alongside existing testHostMemberOfAllLabels)
  • Add unit test for filterExtensionsForHost covering: no extensions, platform-only filtering, label filtering with mixed membership results
  • Add changes file

Test plan

Automated (CI)

  • Existing testHostMemberOfAllLabels still passes (no regression)
  • New testHostMemberOfAllLabelSets covers: empty label sets, single set, multiple sets with mixed membership, nonexistent labels, nonexistent host
  • Unit tests for filterExtensionsForHost cover platform + label filtering

Manual QA

Setup:

  1. Fleet Premium instance with 2+ hosts enrolled via Orbit
  2. Configure 3+ osquery extensions in the agent options, each scoped to different labels

Verify label-scoped filtering still works:

  1. Assign Host A to labels matching Extension 1 and Extension 2 only
  2. Assign Host B to labels matching Extension 2 and Extension 3 only
  3. Wait for the next Orbit config refresh (~30s)
  4. Confirm Host A receives Extension 1 + 2 (not 3)
  5. Confirm Host B receives Extension 2 + 3 (not 1)

Verify no regression on unscoped extensions:

  1. Add an extension with no label scoping
  2. Confirm all hosts receive it regardless of label membership

Verify edge cases:

  1. Extension scoped to a label that does not exist -- extension should be filtered out for all hosts
  2. Extension scoped to an empty label set -- extension should be included for all hosts

Metadata

Metadata

Assignees

Labels

#g-orchestrationOrchestration product group:releaseReady to write code. Scheduled in a release. See "Making changes" in handbook.storyA user story defining an entire feature~postmortem-action-itemIssue is an action item resulting from an incident postmortem.

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
🦤 ‎In review
Status
No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions