budget

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package budget owns the single admission and accounting choke point for GitHub requests (SYNC_ENGINE C-B1..C-B6).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrClosed reports admission attempted after Gate.Close.
	ErrClosed = fmt.Errorf("GitHub budget gate is closed")
	// ErrLeaseLost reports proven loss or expiry of the budget lease.
	ErrLeaseLost = fmt.Errorf("GitHub budget gate lease lost")
)
View Source
var ErrLeaseHeld = errors.New("GitHub installation budget lease is held")

ErrLeaseHeld reports that another process owns the unexpired installation budgeter lease.

Functions

func InsideAdmission

func InsideAdmission(ctx context.Context) bool

InsideAdmission reports whether ctx is executing a Gate before-send hook. Token providers use it only to avoid joining a non-admitted singleflight renewal that could be queued behind the caller's own concurrency slot.

Types

type AuthContext added in v0.4.0

type AuthContext string

AuthContext identifies the credential pool GitHub uses to account a request. Installation tokens and App JWTs have independent REST budgets.

const (
	// InstallationAuth is an installation access token.
	InstallationAuth AuthContext = "installation"
	// AppJWTAuth is a GitHub App JWT.
	AppJWTAuth AuthContext = "app_jwt"
)

type Class

type Class string

Class is the request priority used by the C-B3 reserved floors.

const (
	// Interactive is user-requested work and has no reserved-floor deduction.
	Interactive Class = "interactive"
	// Event is webhook-originated work protected from sweep exhaustion.
	Event Class = "event"
	// Sweep is background reconciliation work.
	Sweep Class = "sweep"
)

type Clock

type Clock interface {
	Now() time.Time
	NewTimerAt(time.Time) (<-chan time.Time, func() bool)
}

Clock is the time source for admission, backoff, and lease-expiry decisions. Tests inject a manual implementation so C-B2/C-B3 timing is deterministic.

type Doer

type Doer interface {
	Do(context.Context, Class, *Request) (*Response, error)
}

Doer is the narrow dependency used by internal/gh. Implementations must preserve Gate.Do's C-B invariants.

type Gate

type Gate struct {
	// contains filtered or unexported fields
}

Gate is the C-B1 per-installation choke point. It owns admission, server-authoritative REST and GraphQL observations, per-auth-context secondary-limit backoff, and the C-B6 concurrency ceiling.

func New

func New(client *http.Client, options Options) *Gate

New constructs an in-process gate. Production callers should use NewLeased; New exists for conformance tests and single-process tooling.

func NewLeased

func NewLeased(
	ctx context.Context,
	client *http.Client,
	gateOptions Options,
	store LeaseStore,
	leaseOptions LeaseOptions,
) (*Gate, error)

NewLeased acquires the Postgres-coordinated C-B1/C-O2 singleton before returning a usable gate. A live lease held by another runtime returns ErrLeaseHeld.

func (*Gate) Close

func (g *Gate) Close(ctx context.Context) error

Close stops admission, keeps renewal alive while admitted calls drain, then snapshots and releases. If the caller's drain deadline expires, stragglers are canceled before bounded cleanup continues (C-B1/C-B6).

func (*Gate) Do

func (g *Gate) Do(
	ctx context.Context,
	class Class,
	req *Request,
) (result *Response, err error)

Do admits and performs exactly one GitHub request (C-B1). REST state comes only from x-ratelimit-* headers; GraphQL state comes only from the supplied rateLimit observer (C-B2/C-B5).

Admission owns a C-B6 concurrency slot until the response body reaches EOF or is closed. Do may return a non-nil Response alongside a non-nil error; callers must still close every non-nil response body. Forgetting to close permanently leaks that slot, and enough leaks stop all installation traffic. Redirect following is disabled because a redirect would otherwise hide an unadmitted request inside http.Client.Do.

An installation-token mint issued from a BeforeSend hook reuses its outer request's concurrency slot so renewal cannot deadlock at MaxConcurrent=1. The mint still performs independent App-JWT REST admission, reservation, header observation, and backoff accounting.

func (*Gate) Snapshot

func (g *Gate) Snapshot() Snapshot

Snapshot returns the latest server observations without mutating them. In-flight reservations are intentionally omitted from persisted state.

type GraphQLRate

type GraphQLRate struct {
	Cost      int64
	Limit     int64
	Remaining int64
	ResetAt   time.Time
}

GraphQLRate is extracted from a response's top-level data.rateLimit block. It is deliberately distinct from REST response-header accounting (C-B5).

type GraphQLRateObserver

type GraphQLRateObserver func(*http.Response) (GraphQLRate, bool, error)

GraphQLRateObserver reads and restores a GraphQL response body, returning the authoritative rateLimit block when one is present.

type LeaseOptions

type LeaseOptions struct {
	InstallationID   int64
	Owner            string
	TTL              time.Duration
	RenewInterval    time.Duration
	SnapshotInterval time.Duration
	StoreTimeout     time.Duration
	Clock            Clock
}

LeaseOptions identifies and times a per-installation budgeter lease. Owner is a diagnostic process name only; an unguessable per-runtime token is the actual database ownership predicate.

type LeaseStore

LeaseStore coordinates the one active budgeter for an installation and persists periodic C-P6 state snapshots. Acquire and Renew return Postgres's authoritative lease expiry; callers must never derive it from local time. Acquire false means the store proved that another unexpired owner holds the lease. For Renew, Save, and SaveBackoff, false means the store has proven that the caller no longer owns the lease. Transport failures must always be returned as errors; returning false for a transport failure violates this contract. Release returns any cleanup failure rather than translating it into ownership loss.

type Options

type Options struct {
	MaxConcurrent int
	RESTLimit     int64
	GraphQLLimit  int64
	// SweepFloor and EventFloor are fractions in (0,1); EventFloor must be
	// lower because event work has priority over sweep work.
	SweepFloor             float64
	EventFloor             float64
	RESTRequestEstimate    int64
	GraphQLPointEstimate   int64
	SecondaryLimitFallback time.Duration
	OnStarvation           StarvationHook
	OnRequest              RequestHook
	Clock                  Clock
	Tracer                 trace.Tracer
}

Options configures a Gate. Limits are initial floor denominators only; an observed server limit always replaces them. Reservations are admission-side pessimism and never replace or persist server-authoritative remaining values (C-B2/C-B3).

type PostgresLeaseStore

type PostgresLeaseStore struct {
	// contains filtered or unexported fields
}

PostgresLeaseStore implements atomic lease acquire/renew/steal-on-expiry and periodic snapshots against installation_budgets.

func NewPostgresLeaseStore

func NewPostgresLeaseStore(pool *pgxpool.Pool) *PostgresLeaseStore

NewPostgresLeaseStore constructs a Postgres-backed lease store.

func (*PostgresLeaseStore) Acquire

func (s *PostgresLeaseStore) Acquire(
	ctx context.Context,
	installationID int64,
	token string,
	ttl time.Duration,
) (Snapshot, time.Time, bool, error)

Acquire obtains or steals an expired installation lease and returns its authoritative persisted snapshot.

func (*PostgresLeaseStore) Release

func (s *PostgresLeaseStore) Release(
	ctx context.Context,
	installationID int64,
	token string,
) error

Release clears an installation lease only for the active owner.

func (*PostgresLeaseStore) Renew

func (s *PostgresLeaseStore) Renew(
	ctx context.Context,
	installationID int64,
	token string,
	ttl time.Duration,
) (time.Time, bool, error)

Renew extends a lease only when token still proves ownership.

func (*PostgresLeaseStore) Save

func (s *PostgresLeaseStore) Save(
	ctx context.Context,
	installationID int64,
	token string,
	snapshot Snapshot,
) (bool, error)

Save persists one budget snapshot only for the active owner.

func (*PostgresLeaseStore) SaveBackoff

func (s *PostgresLeaseStore) SaveBackoff(
	ctx context.Context,
	installationID int64,
	token string,
	authContext AuthContext,
	until time.Time,
) (bool, error)

SaveBackoff immediately persists one auth context's secondary-limit deadline.

type Request

type Request struct {
	// contains filtered or unexported fields
}

Request wraps the only HTTP request shape accepted by Gate.Do. Callers use the constructors below so REST, GraphQL, and App-auth accounting cannot be confused.

func NewAppRESTRequest added in v0.4.0

func NewAppRESTRequest(req *http.Request) *Request

NewAppRESTRequest wraps one App-JWT REST request.

func NewAuthRequest

func NewAuthRequest(req *http.Request) *Request

NewAuthRequest wraps one App-JWT installation-token exchange.

func NewGraphQLRequest

func NewGraphQLRequest(req *http.Request, observer GraphQLRateObserver) *Request

NewGraphQLRequest wraps one GraphQL request and its rate observer.

func NewInstallationRESTRequest added in v0.4.0

func NewInstallationRESTRequest(req *http.Request) *Request

NewInstallationRESTRequest wraps one installation-token REST request.

func NewRESTRequest

func NewRESTRequest(req *http.Request) *Request

NewRESTRequest wraps one installation-authenticated REST request for admission. New call sites should use NewInstallationRESTRequest so the credential context remains explicit.

func (*Request) BeforeSend

func (r *Request) BeforeSend(
	fn func(context.Context, *http.Request) error,
) *Request

BeforeSend installs work that must run after admission and immediately before the transport. GitHub clients use it to refresh and inject an installation token without letting a queued request carry a stale token.

type RequestHook

type RequestHook func(RequestObservation)

RequestHook is M6's C-B1/C-B4 request-rate and conditional-hit seam.

type RequestObservation

type RequestObservation struct {
	Class          Class
	Resource       Resource
	AuthContext    AuthContext
	EndpointFamily string
	StatusCode     int
	Conditional    bool
	NotModified    bool
	Err            error
}

RequestObservation is emitted after one admitted network call. It contains only cardinality-bounded accounting data and never request URLs or headers.

type Resource

type Resource string

Resource identifies GitHub's independently-accounted API resources. REST accounting is further partitioned by AuthContext.

const (
	// REST is GitHub's REST request budget.
	REST Resource = "rest"
	// GraphQL is GitHub's installation GraphQL point budget.
	GraphQL Resource = "graphql"
)

type ResourceBudget

type ResourceBudget struct {
	Known     bool
	Limit     int64
	Remaining int64
	ResetAt   time.Time
}

ResourceBudget is the most recently observed server-authoritative budget. Known is false until a complete REST header set or GraphQL rateLimit block has been observed.

type Response

type Response struct {
	HTTP        *http.Response
	GraphQLRate *GraphQLRate
}

Response preserves the HTTP response and, for GraphQL, the extracted point accounting observed before the concurrency slot is released.

type Snapshot

type Snapshot struct {
	REST               ResourceBudget
	AppREST            ResourceBudget
	GraphQL            ResourceBudget
	BackoffUntil       time.Time
	AppJWTBackoffUntil time.Time
	InFlight           int
}

Snapshot is safe to expose to persistence and observability code. It never contains credentials or request data.

type Starvation

type Starvation struct {
	Class       Class
	Resource    Resource
	AuthContext AuthContext
	Remaining   int64
	Limit       int64
	ResetAt     time.Time
}

Starvation is emitted once for each request that queues behind a C-B3 reserved floor.

type StarvationHook

type StarvationHook func(Starvation)

StarvationHook is the M1 observability seam; M6 will attach metrics.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL