Documentation
¶
Overview ¶
Package runbudget bounds the load one run may place on a source provider, shared across every consumer that touches the same provider quota.
Experimental: the admission contract will churn while its consumers are built.
The problem it exists for: a run that enumerates in N parallel lanes and reads content from those lanes has N places that can issue provider requests, and a provider that rate-limits does not care how the client is structured. Sizing concurrency per lane makes lane count a load multiplier, so adding lanes stops being a topology choice and starts being a way to get throttled. Here, lane count is topology: consumers draw on one budget per quota domain, so adding lanes redistributes work rather than enlarging the fleet.
The package is neutral by construction. It knows nothing about plans, lanes, probes, or transfers — only that some number of consumers want to start requests against some quota domain — so the enumerating side and the mutating side can both depend on it without depending on each other.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Budget ¶
type Budget struct {
// contains filtered or unexported fields
}
Budget admits provider requests against per-domain and run-global ceilings.
One Budget serves every consumer of a run. Admission is fair by arrival within each domain: a blocked waiter holds back younger waiters that need any of the same domains, and only those. Serving whoever happens to fit would let a stream of small requests starve a large one indefinitely, and a starved consumer is a lane that never finishes; ordering globally instead would let one congested quota stall traffic to every unrelated quota in the run.
func (*Budget) Acquire ¶
Acquire blocks until the run may start the request, and returns a lease that holds its share of the budget until retired.
It honors context cancellation while waiting. A cancelled waiter leaves the queue without consuming anything; if it is cancelled in the same moment it is admitted, the admission is released rather than leaked.
func (*Budget) Throttle ¶
Throttle records that a provider throttled a request against this domain.
The response is shared state, not per-caller: every consumer drawing on the domain slows down together, and retryAfter pauses new starts for all of them. That is the difference between one coordinated backoff and N consumers each discovering the same throttle independently.
An unusable domain is an error rather than a silent no-op. Discarding it would make provider feedback disappear at exactly the moment it matters, and it is inconsistent with refusing the same domain on the request path.
type Domain ¶
type Domain struct {
// Version is the provider's domain-naming scheme. A provider that changes
// how it names domains bumps this, and domains of different versions are
// never equal — so a scheme change degrades to separate budgets rather than
// to a silently wrong sharing decision.
Version int `json:"version"`
// ID names the quota within that scheme.
ID string `json:"id"`
}
Domain identifies a provider quota: the scope within which requests contend with each other for rate and concurrency.
It is supplied by a provider adapter and opaque here. Only the provider knows what its service actually meters — one account, one bucket, one endpoint, or some combination — so this package never parses an ID, normalizes it, or infers that one domain contains another. Inferring a hierarchy would be this package guessing at provider policy.
Equality is exact over both fields. Two requests share a budget if and only if their domains are equal.
A Domain is deliberately not the plan's base identity. Base identity answers "is this the same work" and belongs to resume; a quota domain answers "does this request contend with that one". A run enumerating one bucket may draw on a domain covering an entire account, and two runs against different buckets may share one. Neither is derivable from the other.
A Domain must never carry credentials. It describes service topology, not the principal making the request.
func (Domain) String ¶
String renders a domain for diagnostics.
The ID is quoted. It is provider-supplied opaque text that reaches logs, so an ID carrying newlines or control characters could otherwise forge log lines.
func (Domain) Validate ¶
Validate reports whether a domain is usable as a budget key.
It checks that an ID exists and survives serialization unchanged; it does not trim, case-fold, or otherwise normalize a valid ID. Exact equality is the contract, so altering the bytes would silently change which requests contend with each other — and a whitespace-only ID is a legal opaque value, not an absent one.
type DomainUsage ¶
type DomainUsage struct {
Domain Domain
InFlight map[OpClass]int
OpenBodies int
RateFactor float64
}
DomainUsage is a point-in-time view of one domain's provider-scoped usage.
type Lease ¶
type Lease struct {
// contains filtered or unexported fields
}
Lease is one admitted request's hold on the budget.
Its three reservations retire independently, because they do not end together: a provider call can complete while its body is still being streamed, and a body can close while a buffer decoded from it is still live. A lease that returned them as one would make a consumer choose between releasing a ceiling it still needs and holding two it does not.
Every retirement is idempotent, and Release is the fail-safe that returns whatever is left — so a deferred Release after explicit retirement is correct rather than a double-count.
func (*Lease) CloseBody ¶
func (l *Lease) CloseBody()
CloseBody retires the open-body slot, leaving the request slot and memory reservation held.
A body slot models a held provider connection, so it is charged until the body is actually closed. A consumer that streams a response should close it as soon as it is drained; holding it until Release would ceiling the run on bodies it is no longer reading.
func (*Lease) CompleteRequest ¶
func (l *Lease) CompleteRequest()
CompleteRequest retires the in-flight request slot, leaving any open body and memory reservation held.
func (*Lease) Release ¶
func (l *Lease) Release()
Release retires everything the lease still holds.
It is the fail-safe, not the ordinary path: a consumer that knows when each resource is done should retire them as they finish. It is idempotent, so deferring it is always safe.
func (*Lease) ReleaseMemory ¶
func (l *Lease) ReleaseMemory()
ReleaseMemory retires the buffer reservation, leaving the request slot and any open body held.
type Limits ¶
type Limits struct {
// RequestsPerSecond bounds the rate at which requests may start against one
// quota domain.
RequestsPerSecond float64
// Burst is how many request starts may bunch together on one domain. It
// defaults to one second's worth of rate when a rate is set and this is
// zero, because a bucket with no burst admits nothing.
Burst int
// InFlight bounds concurrent requests per operation class, per domain.
// Classes absent from the map are unbounded; an unknown class is refused by
// New.
InFlight map[OpClass]int
// OpenBodies bounds response bodies held open at once against one domain. A
// body is charged until it is closed, not until the request completes,
// because that is how long the provider connection is actually held.
OpenBodies int
// MemoryBytes bounds buffer reservations held concurrently across the whole
// run. Unlike the others this is not per domain — see the type comment.
MemoryBytes int64
}
Limits is the ceiling a run places on its own work.
The dimensions are deliberately not all the same scope, because the resources they describe are not the same kind of resource:
- RequestsPerSecond, Burst, InFlight and OpenBodies are provider-scoped, and apply per quota domain. They describe load leaving the process, which is metered by whatever the provider meters.
- MemoryBytes is run-global. It describes buffers held inside this process, which one heap holds regardless of how many providers a run talks to. A per-domain memory ceiling would not be a ceiling at all: two disjoint domains would each be entitled to the whole limit, and a request spanning several domains would be charged for one buffer in each of their ledgers.
A zero value in any dimension means that dimension is unbounded. This is deliberate — a caller bounding only memory should not have to enumerate every operation class — but an all-zero Limits bounds nothing, which New refuses so that "no budget" is never reached by omission.
type OpClass ¶
type OpClass string
OpClass is the kind of provider request being started.
Classes are accounted separately because they are not interchangeable load: a listing and a ranged read of a large object consume different provider resources, and a budget that pooled them would let one starve the other while reporting healthy.
const ( // OpList is an object listing request. OpList OpClass = "list" // OpHead is a metadata request. OpHead OpClass = "head" // OpGet is a whole-object or ranged content read. OpGet OpClass = "get" // OpCopySourceRead is the source-side read a server-side copy performs. // It is distinct from OpGet because it is charged to the source provider // without the client ever holding the body. OpCopySourceRead OpClass = "copy_source_read" )
type Request ¶
type Request struct {
// Domains are the quota domains this request consumes. A provider that
// meters more than one scope at once — say a per-account rate and a
// per-bucket rate — returns all of them, and the request is admitted only
// when every one of them can admit it. It does not pick one.
Domains []Domain
// Class is the kind of request.
Class OpClass
// MemoryBytes is the buffer reservation held until it is released. It is
// charged once against the run, not once per domain: one buffer is one
// buffer however many quotas the request touches.
MemoryBytes int64
// OpensBody declares that the request will hold a response body open.
OpensBody bool
// Consumer names the caller for diagnostics — a lane, a worker pool. It has
// no effect on admission: fairness here is by arrival, not by consumer,
// because weighting consumers would make lane count matter again.
Consumer string
}
Request describes one provider request a consumer wants to start.
type Usage ¶
type Usage struct {
Domains []DomainUsage
MemoryHeld int64
Waiters int
}
Usage is a point-in-time view of the whole budget, for progress reporting and for evidence that consumer count did not multiply provider load.
MemoryHeld sits here rather than on DomainUsage because it is run-global.