ReqManagedAgents. Providers. BedrockAgentCore. WaitBudget
(ReqManagedAgents v0.12.0)
Copy Markdown
View Source
The single wall-clock budget a BedrockAgentCore.provision/2 call spends across
all of its waits and every request they make.
One provision can wait twice — first for a same-name harness to finish deleting,
then for the new one to reach READY. Bounding each wait by a poll count made
those budgets additive and left the wall-clock cost unbounded in both directions:
a count says nothing about how long one poll's HTTP call takes, and two counted
waits spend two full budgets. A %WaitBudget{} is built once at the entry to
provision/2 and threaded through every wait, so deadline_at is one absolute
monotonic instant that all of them share.
The two budget shapes
:timeout (ms) is the whole budget, and the deadline alone bounds the loops.
The legacy :ready_poll_ms / :ready_max_polls pair is still accepted and keeps
its count semantics, under a deadline derived from poll_ms * (max_polls + 1)
— every poll the count allows, so the count is what binds — floored at the
default. The floor matters: ready_poll_ms: 0 means "poll as fast
as you can, N times" — attempts, not time — and deriving a zero-length wall-clock
budget from it would end the wait after a single poll.
An explicit :timeout with no :ready_poll_ms also derives its own cadence,
scaled to the budget and never wider than the default. A budget shorter than one
default interval would otherwise buy exactly one poll — creating a harness,
looking at it once and rolling it back, with the arithmetic entirely correct.
Why next/1 stops early
next/1 returns :poll only while a whole poll interval still fits inside what
is left, not merely while time remains. A wait must stop while it still has time
to return its named error tuple; overrunning the deadline and being killed from
outside is the failure this module exists to prevent.
Summary
Types
deadline_at is an absolute System.monotonic_time(:millisecond) instant, not a
duration — it is what makes the budget shareable across two sequential waits.
polls_left is :infinity when the caller gave an explicit :timeout.
Whether a wait may poll once more, or has run out of budget.
Functions
The receive_timeout one HTTP attempt of a poll may use.
Builds the budget for one provision/2 call from its opts, starting the clock.
Whether the wait may sleep and poll once more.
Milliseconds left before the deadline; negative once it has passed.
Records one poll against the legacy count. A :timeout-shaped budget is unchanged.
Types
@type opt_key() :: :timeout | :ready_poll_ms | :ready_max_polls
The opts this budget reads, and the keys new/1 can reject.
@type t() :: %ReqManagedAgents.Providers.BedrockAgentCore.WaitBudget{ deadline_at: integer(), poll_ms: non_neg_integer(), polls_left: non_neg_integer() | :infinity }
deadline_at is an absolute System.monotonic_time(:millisecond) instant, not a
duration — it is what makes the budget shareable across two sequential waits.
polls_left is :infinity when the caller gave an explicit :timeout.
@type verdict() :: :poll | :expired
Whether a wait may poll once more, or has run out of budget.
Functions
@spec attempt_timeout(t(), timeout(), pos_integer()) :: pos_integer()
The receive_timeout one HTTP attempt of a poll may use.
Bounded by the smaller of the client's configured timeout and what is left of the
budget, then divided by attempts — the poll's transport retries the request, so
a per-attempt timeout equal to the whole remaining budget would let one hung poll
spend it several times over. Never returns less than 1: a poll on a spent budget
degrades to "try once, briefly", and a non-positive receive_timeout is rejected
by the transport.
What this does not cover
receive_timeout bounds the receive phase of one attempt. Three costs sit
outside it, so a poll can exceed its share of the budget:
- the transport's sleeps between retries (exponential backoff — roughly 1 s
then 2 s at the default policy), and a
Retry-Afterheader, which the server chooses; - the connect timeout, which is a separate 30 s default;
- the time to establish TLS on a cold connection.
A wait therefore overruns its deadline by at most one poll's envelope, and the smaller the budget the larger that envelope is in proportion — at a 200 ms budget, one transient error costs seconds. Bounding the total needs a deadline-aware retry delay, which belongs in the client rather than here.
Builds the budget for one provision/2 call from its opts, starting the clock.
Reads :timeout (ms) and the legacy :ready_poll_ms / :ready_max_polls. Each
must be a non-negative integer; anything else returns
{:error, {:invalid_opts, key}}. That includes timeout: :infinity, which this
budget deliberately cannot express — a wait with no deadline is the defect it
exists to prevent.
Whether the wait may sleep and poll once more.
:poll requires both that the legacy count is not exhausted and that a whole
poll_ms sleep fits inside the time left.
Milliseconds left before the deadline; negative once it has passed.
Records one poll against the legacy count. A :timeout-shaped budget is unchanged.