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
Engineering
Test plan
Automated (CI)
Manual QA
Setup:
- Fleet Premium instance with 2+ hosts enrolled via Orbit
- Configure 3+ osquery extensions in the agent options, each scoped to different labels
Verify label-scoped filtering still works:
- Assign Host A to labels matching Extension 1 and Extension 2 only
- Assign Host B to labels matching Extension 2 and Extension 3 only
- Wait for the next Orbit config refresh (~30s)
- Confirm Host A receives Extension 1 + 2 (not 3)
- Confirm Host B receives Extension 2 + 3 (not 1)
Verify no regression on unscoped extensions:
- Add an extension with no label scoping
- Confirm all hosts receive it regardless of label membership
Verify edge cases:
- Extension scoped to a label that does not exist -- extension should be filtered out for all hosts
- Extension scoped to an empty label set -- extension should be included for all hosts
Goal
filterExtensionsForHostto check all extension label requirements in a single DB query,Context
Parent story: #35467
GetOrbitConfigis called every ~30 seconds by every Orbit-enrolled host. Part of that code path isfilterExtensionsForHost(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 joininglabelsandlabel_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)
At larger fleet sizes or more extensions, the savings grow linearly.
Current code (simplified)
Changes
Product
Engineering
HostMemberOfAllLabelSets(ctx, hostID, labelSets map[string][]string) (map[string]bool, error)that checks membership for multiple label sets in a single queryserver/fleet/datastore.goserver/datastore/mysql/labels.gomake generate-mockfilterExtensionsForHostinserver/service/orbit.goto collect all extensions' label requirements, call the new batch method once, then filter based on the resultsHostMemberOfAllLabelSets(alongside existingtestHostMemberOfAllLabels)filterExtensionsForHostcovering: no extensions, platform-only filtering, label filtering with mixed membership resultsTest plan
Automated (CI)
testHostMemberOfAllLabelsstill passes (no regression)testHostMemberOfAllLabelSetscovers: empty label sets, single set, multiple sets with mixed membership, nonexistent labels, nonexistent hostfilterExtensionsForHostcover platform + label filteringManual QA
Setup:
Verify label-scoped filtering still works:
Verify no regression on unscoped extensions:
Verify edge cases: