Provider wiring¶
3.1 AWS¶
install_aws_hooks(session, account_id, region, default_config=...) (resilience/aws.py) registers four handlers on a boto3 or aioboto3 session's event emitter. Every client created from that session afterwards is covered, paginators included, without touching the collectors:
| Event | What the hook does |
|---|---|
before-call |
Resolves the scope, fails fast with AWSCircuitOpenError if the scope's breaker is open, counts the call, takes a limiter token, then a slot of the account's bulkhead |
needs-retry |
A throttled attempt halves the scope's rate and honours Retry-After; transient errors are counted. Every retry botocore is about to make, throttled or transient, costs one token of the retry budget, except the first two retries of the call; when the budget is empty the hook raises AWSRetryBudgetExhaustedError, which ends botocore's retry loop. A throttled retry also waits for a fresh rate token, on top of botocore's own backoff |
after-call |
Releases the bulkhead slot. 2xx/3xx feeds additive recovery and closes the breaker. An error that survived botocore's retries goes to the governor's on_final_error: throttling counts toward the breaker and is recorded as given up (stats and the task's ledger), a transient error counts toward the breaker, and a FATAL provider answer counts as proof that the service is reachable |
after-call-error |
The same for exceptions (connection errors and the like). A spent retry budget is recorded as given up without a breaker failure, and a call the open circuit refused is not recorded again |
aiobotocore awaits coroutine handlers, so the aioboto3 hooks never block the event loop. Plain boto3 sessions get blocking handlers; when such a call runs on an event-loop thread, the token wait is capped at one second (MAX_INLINE_WAIT) so the loop is not stalled. The token is still consumed.
Installation is idempotent (a marker on the botocore session) and never breaks authentication: any error while installing is logged at DEBUG and the session is used as is. Where it is installed:
AsyncAWSCollector._get_aio_session(): the aioboto3 session behind every core and deep collector (all 133 deep collector tasks), the Cloud Control and tagging sweeps and the per-item fan-out, scoped to the collector's account and region.credentials.build_aws_session(): the base boto3 session (account*, since the account is not known yet) and, after STS AssumeRole, the assumed session (with the target account id). That covers STS, Organizations and Control Tower discovery and the per-account sessions of the organization fan-out.
default_config becomes the session's default client config when it has none, so clients created without config= still get the configured retries.
botocore keeps doing the per-request retrying. Its settings now come from aws.retry_mode and aws.max_retries, which earlier releases accepted in the config but ignored (cloudg hard-coded adaptive mode and 10). The defaults are unchanged. botocore reads max_attempts in the retries dict as the number of retries after the first attempt, so the default max_retries: 10 allows up to 11 attempts per call; the integration tests check exactly that (with max_retries: 3, a throttled operation is sent four times). ratelimit.aws.max_retries, max_backoff_seconds and deadline_seconds are different settings: they bound cloudg-level retries in call_with_resilience, not botocore's. Per-call time limits on AWS stay botocore's job (its connect and read timeouts together with the retry count).
3.2 Azure Resource Manager¶
Every azure-mgmt client cloudg builds goes through build_azure_client(cls, ...) (resilience/azure.py): the collector's per-kind clients, resource_management_client, subscription_client and the Resource Graph client. It adds two keyword arguments to the constructor:
retry_policy: an explicit azure-coreRetryPolicy(retry_total=8, retry_status=8, retry_backoff_max=60, timeout=900), taken fromratelimit.azure.max_retries,max_backoff_secondsanddeadline_seconds. azure-core's default allows only 3 status retries; itsRetryPolicyalready honoursRetry-Afteron 429 and 503. The values were checked on a real ARM client;per_retry_policies=[AzureThrottlePolicy], which runs around every HTTP attempt, retries included.
The policy resolves the scope from the request URL (subscription and the last Microsoft.<Namespace> segment), fails fast on an open breaker, counts the call, takes a token (subscription bucket, then the resource-provider leaf) and a slot of the subscription's bulkhead. On the response, or on an exception, the slot is released, and then:
- 429 or 503: the scope's rate is halved and paused for
Retry-After, or forx-ms-user-quota-resets-afterwhen that is the only hint; - 429, 503, 408, 500, 502 or 504: the response costs one token of the retry budget, because azure-core is about to retry it (the first two retries of a request are free). When the budget is empty the policy raises
RetryBudgetExhaustedError, which ends azure-core's retry loop and is reported as throttling. The token is taken even on the last attempt, which azure-core would not retry; - other 4xx: nothing;
- success: additive recovery and breaker reset, then two proactive checks.
x-ms-user-quota-remaining: 0pauses the Resource Graph scope untilx-ms-user-quota-resets-after.x-ms-ratelimit-remaining-subscription-readsbelow 25 pauses the subscription's aggregate bucket for one second, which holds every call of that subscription, whatever the resource provider, because ARM refills about 25 tokens a second. With no account limit configured, the pause falls back to the resource provider's bucket.
If a client class rejects the extra keyword arguments (an old SDK, a test double), build_azure_client logs at DEBUG and builds it plainly. When a service still fails, the collector records service_errors[name] = describe_error(exc); for throttling it also reports the scope as given up so the breaker counts it. MultiAccountCollector turns each service error into an azure_<service> coverage record with status FAILED and marks azure_full PARTIAL.
3.3 Azure Resource Graph¶
Resource Graph allows 15 queries per 5-second window per user, so it has its own scope, azure/*/*/resourcegraph (no subscription: the quota follows the caller, not the subscription), at 3 rps with a burst of 15.
_fetch_page in cloudg/inventory/azure_graph/query.py handles one page:
- Clients from
default_graph_client_factoryare built withbuild_azure_client, so the throttle policy paces every HTTP attempt inside the SDK pipeline and reads the quota headers._fetch_pagenotices that (_cloudg_throttle_policyon the client) and does not take a second token. - Clients from a custom factory are paced by
_fetch_pageitself: breaker check and one token per page. - A client built with the throttle policy already retries 429 inside its azure-core pipeline, so a 429 that reaches
_fetch_pagefrom it is final: it is recorded (on_final_error) and raised, not retried a second time. - For a client from a custom factory, a 429 slows the scope down and is retried up to
QueryLimits.max_retries(5) times, with a back-off of 2, 4, 8, 16 then 30 seconds. The limiter, which is paused for the server's hint, waits the hint out; with the limiter switched off the back-off itself is stretched to the hint, capped at 300 seconds. After the last retry the query is recorded as given up and the error is raised.
3.4 GCP Cloud Asset Inventory¶
GCPCollector._iterate drains one paged call (ListAssets, SearchAllResources or SearchAllIamPolicies) in a worker thread:
- breaker check, one token and one counted call for
gcp/<parent>/*/cloudasset/<Rpc>, then one bulkhead slot for the whole listing (max_concurrencyconcurrent listings per project or organization); - the call itself, with
retry=gcp_retry(scope, counter=...): a google-api-coreRetryonResourceExhausted,ServiceUnavailable,DeadlineExceededandInternalServerError, starting at 1 second, doubling, capped atratelimit.gcp.max_backoff_seconds(60), with an overall timeout ofdeadline_seconds(900). Itson_errorreports each retried error to the governor, so throttling halves the rate andRetryInfodelays pause it. It also enforces two limits api_core does not have: each retry after the first two of a page costs a retry-budget token, and at mostratelimit.gcp.max_retries(8) consecutive retries are made per page (aRetryCountershared withpacedresets on every page that arrives). Past either limit,on_errorraisesRetryBudgetExhaustedError, chained to the provider error, which ends api_core's retry loop; paced(...)around the pager takes a token everypage_sizeitems, just before the pager fetches the next page. It does not check the breaker again: the breaker was checked once for the whole listing in step 1, so a listing that is a half-open breaker's probe is not rejected by its own breaker on page 2. Pages are fetched lazily while iterating, so this keeps org-wide listings under the per-minute quotas. With short pages it is approximate, never off by more than one page per short page.
If the listing still fails, throttling is recorded as given up and transient errors count toward the breaker. _iterate returns what it collected so far plus the error, and the caller records listing truncated: throttled: ... (PARTIAL), or FAILED when nothing came back, and falls back to SearchAllResources when ListAssets is unavailable. GCPDeepInventoryCollector labels its IAM policy search the same way.