Skip to content

feat: add optional queryInterval to throttle Microsoft Graph queries#118

Merged
ytsarev merged 4 commits into
mainfrom
query-interval
Jul 16, 2026
Merged

feat: add optional queryInterval to throttle Microsoft Graph queries#118
ytsarev merged 4 commits into
mainfrom
query-interval

Conversation

@ytsarev

@ytsarev ytsarev commented Jul 16, 2026

Copy link
Copy Markdown
Member

What

Adds an optional queryInterval input field (a Go duration string, e.g. "10m"). When set, the function records the timestamp of its last successful query and skips querying Microsoft Graph again until the interval has elapsed — independent of how often the Composition reconciles. This mitigates Microsoft Graph API throttling.

How

  • New queryInterval *string input, parsed with time.ParseDuration (invalid/non-positive → fatal result).
  • On a successful query the timestamp is embedded as a lastQueryTime element in the result written to the status. target. A composition function can't mutate XR metadata, so the status target is the only durable, function-writable state to read back on the next reconcile.
  • Before querying, the function reads that timestamp back from the observed XR status and, while the interval hasn't elapsed, skips with a FunctionSkip/IntervalLimit condition. The current time comes from the existing TimerInterface, keeping the logic deterministic under test.
  • Scope: Composition mode + status. targets (context isn't persisted across reconciles; Operation mode writes annotations, not status).
  • Regenerated deepcopy + packaged CRD (also picks up the previously-missing failOnEmpty field).

Testing

Unit

Added table-driven cases: skip within interval, refresh after elapsed, first-run embed, invalid duration. go test ./..., go vet, and golangci-lint (gocyclo ≤ 10) all pass.

Local render (crossplane render)

  • run+embed → query returns data with lastQueryTime appended to status.validatedUsers.
  • skip → FunctionSkip/IntervalLimit, status preserved.

macOS note: used -r, not -rc. -c/--include-context hangs under crossplane CLI v2.x + Docker Desktop (unix socket over the render container) — known bug crossplane/cli#161, fixed on main by #163 but unreleased.

Cluster e2e (kind + Crossplane 2.3.3, function-msgraph:v0.7.0-rc3)

Composition with queryInterval: "2m"status.validatedUsers:

Behavior Result
First run Queried Graph, embedded lastQueryTime
Within interval FunctionSkip/IntervalLimit, status unchanged
After interval elapses Re-queried, lastQueryTime advanced
Independent of reconcile frequency queries=2 across ~16 reconciles (skips=14) — one Graph call per 2m window

Docs & examples

  • README: "Throttling Mitigation" section + config-table row.
  • example/user-validation-example-query-interval.yaml, example/xr-with-last-query-time.yaml.
  • example/e2e/ — runnable cluster e2e manifests + README.

ytsarev added 3 commits July 16, 2026 12:28
Add a `queryInterval` input field (a Go duration string, e.g. "10m") that
lets the function skip querying Microsoft Graph until the interval has
elapsed since its last successful query, independent of how often the
Composition reconciles. This helps avoid Graph API throttling.

Because a composition function cannot mutate XR metadata, the last-query
timestamp is persisted as a `lastQueryTime` element appended to the result
stored at the status target and read back on subsequent reconciles. The
current time is sourced from the existing TimerInterface to keep the logic
deterministic under test. Interval limiting is effective in Composition mode
with a "status." target only; invalid durations are surfaced as a fatal
result.

Also regenerate the deepcopy and packaged CRD (which additionally picks up
the previously missing failOnEmpty field).

Signed-off-by: Yury Tsarev <yury@upbound.io>
Document how to exercise queryInterval with `crossplane render`:

- Add a "Query Interval (Throttling)" section to example/README.md with
  runnable render commands for both the run+embed and skip scenarios.
- Add example/xr-with-last-query-time.yaml, an observed XR carrying a
  lastQueryTime so the skip path can be demonstrated deterministically.
- Use `-r` (not `-rc`) and add a macOS note: `-c`/--include-context hangs
  under crossplane CLI v2.x + Docker Desktop because the context function
  is reached over a unix socket bind-mounted into the render container.
  This is crossplane/cli#161, fixed on main by #163 but not yet released.

Signed-off-by: Yury Tsarev <yury@upbound.io>
Add example/e2e/ with a runnable Composition-mode end-to-end setup for the
queryInterval feature: a Function install manifest, a Composition using
queryInterval: "2m" targeting status.validatedUsers, an XR, and a short README
covering run/verify steps. Reuses example/definition.yaml (XRD) and the local
azure-creds secret (kept out of git).

Validated on kind + Crossplane 2.3.3 with function-msgraph:v0.7.0-rc3: first
run queries and embeds lastQueryTime, reconciles within the interval skip
(FunctionSkip/IntervalLimit), and the query refreshes only after the interval
elapses (queries=2 across ~16 reconciles).

Signed-off-by: Yury Tsarev <yury@upbound.io>
@ytsarev
ytsarev requested a review from jakubramut July 16, 2026 12:23
Comment thread fn.go Outdated
Comment thread fn.go
…ined

skipQueryWhenTargetHasData takes precedence once the target has data, which
silently suppresses the queryInterval refresh. Surface this at runtime with a
non-fatal warning instead of relying on the README alone, so a conflicting
configuration is visible without breaking existing setups.

Adds the warning in validateAndPrepareInput, a unit test, and a README note.

Signed-off-by: Yury Tsarev <yury@upbound.io>

@jakubramut jakubramut left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@ytsarev
ytsarev merged commit 5c1a44b into main Jul 16, 2026
5 checks passed
@ytsarev
ytsarev deleted the query-interval branch July 16, 2026 16:10
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.

2 participants