Skip to content

Skip policy_membership writes when passes value is unchanged #44191

Description

@getvictor

Problem

RecordPolicyQueryExecutions in server/datastore/mysql/policies.go runs on every osquery distributed-write that includes policy results. It UPSERTs every incoming (policy_id, host_id, passes) triple via INSERT INTO policy_membership ... ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at), passes=VALUES(passes), regardless of whether passes actually changed since the previous run. With N hosts and M policies-per-host, every check-in writes N×M rows even when steady-state passing/failing rates are stable.

The same function already does the read needed to skip the write. FlippingPoliciesForHost selects the previous (policy_id, passes) rows from policy_membership and computes both newFailing and newPassing — the exact set of rows that actually need a passes change. The current code only consumes newPassing (for retry-reset bookkeeping) and discards newFailing, so it falls back to UPSERTing everything.

Impact

This path's cost depends on the stability of each host's policy results. First-time submissions (right after enrollment, or after a policy is added or its query changes) genuinely need to write — there is no prior row to compare against. Steady-state submissions on a stable host population are mostly no-op UPSERTs: passes matches what's already in the row.

The 2026-04-25 loadtest on victor-484 captured both regimes. During the enrollment wave that scaled the fleet from 10,500 to 20,000 hosts (~19:55–20:10Z), INSERT INTO policy_membership ... ON DUPLICATE KEY UPDATE peaked at ~1,277 s of writer wall in a single 5-min bucket — about 4.3 s of writer wall per second of clock, dominating every other writer that interval. Once the wave settled, the same statement fell to ~17 s over a 15-min window and dropped to ~#13 in the writer top-15, well below ambient writes like scheduled_query_stats and host_seen_times.

The fix does not change the enrollment-burst cost — those inserts have to land. What it removes is the UPSERT cost for unchanged-passes rows, which is the dominant case in long-running operation: a Fleet that's been up long enough for hosts to have submitted at least one round of policy results, with stable pass/fail rates, sees most RecordPolicyQueryExecutions writes become no-ops without this change. The savings compound over time and reduce writer-side amplification when an enrollment wave or policy edit lands on top of ambient load.

Proposed fix

Use the result of FlippingPoliciesForHost to build the UPSERT batch only from newFailing ∪ newPassing instead of all incoming results. Rows whose previous passes value matches the incoming value are skipped — they're true no-ops.

Two specifics:

The current code calls FlippingPoliciesForHost for the retry-reset path but throws away newFailing. Capture both and use their union. In the SubmitDistributedQueryResults path that pre-computes newlyPassingPolicyIDs and passes it in (so the inner call is skipped), pass both newFailing and newPassing through to RecordPolicyQueryExecutions, or fall back to a fresh FlippingPoliciesForHost read locally — the read is small (single host, small policy count) and the write savings dominate.

updated_at semantics shift slightly: today it refreshes every check-in regardless of state change; after the fix it refreshes only when passes actually flips. This is arguably more useful (it now means "last time membership state changed"), but verify no consumer reads updated_at as a "last seen" timestamp. The existing policy_updated_at on the hosts table already serves the per-host last-policy-run signal and is unchanged by this fix.

Evidence

Loadtest writer top-ops (2026-04-25, victor-484): INSERT INTO policy_membership ... ON DUPLICATE KEY UPDATE peaked at ~1,277 s in a single 5-min bucket and ~931 s over a 15-min window during the 19:55–20:10Z enrollment wave (10,500 → 20,000 hosts). After the wave settled, the same statement fell to ~17 s over a 15-min window and dropped out of the writer top-15 entirely.

Existing flipping-detection that already does the work to identify writeable rows: FlippingPoliciesForHost in server/datastore/mysql/policies.go.

Current write site: RecordPolicyQueryExecutions in server/datastore/mysql/policies.go.

Related top-2 reader, also touching policy_membership: SELECT SUM(1 - pm.passes) AS n_failed FROM policy_membership pm WHERE pm.host_id = ? at ~107 s wall over the same window.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions