pool

package
v0.27.1 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package pool reuses one provider per endpoint, so several components asking for the same forge get one connection between them rather than one each.

What it is for

A provider is built per component today, not per host. In a consumer of any size that means the same forge is constructed several times over: each construction opens its own connection pool and completes its own TLS handshake to a host the process is already talking to, and each re-resolves the credential — which may reach a keychain, Vault or a remote secret store. The components then use capabilities of what is, as far as the forge is concerned, the same connection.

A pool collapses that to one. It is a value a consumer constructs, holds and passes — never package state — so sharing is visible in the wiring and a component handed nothing shares nothing.

A pool is scoped to a unit of work, not to a process

This is the constraint to read before holding one anywhere.

A forge provider captures its credential when it is BUILT: every adapter in this estate resolves a token at construction and holds the resulting string. Nothing refreshes it and nothing can — a credential source may return a token with an expiry, and the provider has no way to learn it has lapsed. That is unlike a cloud SDK configuration, which refreshes underneath its holder and is therefore safe to memoise indefinitely.

Reuse extends a captured credential's life from one component's work to the pool's. So a pool belongs to a unit of work: a command for a CLI, one cycle for a daemon. A fresh pool per cycle is correct rather than wasteful — the TRANSPORT is what carries connection reuse across cycles, and a transport holds no credential. Pass one with forge.WithHTTPTransport and let the pool be short-lived.

Pool.Invalidate exists for a consumer that learns a credential has lapsed mid-work. Read its documentation before wiring it: invalidating on the wrong signal is worse than not invalidating at all.

Cost

One module, gitlab.com/phpboyscout/go/clientlifecycle, which has no dependencies of its own. It is a separate package from the forge contract so that anyone merely authoring a provider pays nothing for machinery they do not use.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Pool

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

Pool reuses one forge.Provider per forge.Endpoint.

The zero value is not usable; construct one with New. Safe for concurrent use.

func New

func New(cfg forge.Config, opts ...forge.Option) *Pool

New returns a pool that builds providers from cfg.

Why the configuration is fixed here

It is deliberately not a parameter of Pool.Provider. A pool answers from a memo, so a configuration passed per call would be read on the first call and silently ignored on every later one — a parameter that sometimes does nothing is the shape of the bug, written into the signature.

A consumer needing two configurations builds two pools. That is also how two credentials for one host stay apart when they are not separated by forge.Endpoint.Name: separate pools, separate memos, nothing shared by accident.

opts are passed to every factory this pool calls. forge.WithHTTPTransport belongs here: it is what lets a short-lived pool still reuse connections across the units of work that follow it.

func (*Pool) Invalidate

func (p *Pool) Invalidate(ep forge.Endpoint)

Invalidate discards the provider held for ep, so the next request rebuilds.

Invalidate only on asserted credential-invalidity

Not on "the operation failed". The underlying resolver runs at most one attempt per generation and a generation is created by each invalidation, so invalidating on the wrong signal does not merely retry — it fans concurrent resolutions at a forge that is already refusing.

The deciding property is NOT whether the failure looks transient. A rate limit is transient, and invalidating on one aims concurrent credential resolutions at an API that is already throttling, which is how a throttle becomes a lockout:

invalidate  — the error asserts THIS CREDENTIAL is no longer valid
do not      — the credential is valid and the caller lacks permission (it would loop)
do not      — the caller is rate limited (back off)
do not      — "the call failed", whatever its status

Being able to draw that distinction is a PREREQUISITE for wiring this in, not an improvement to it. forge.ErrUnauthorized is the sentinel that asserts it, and it is the only one of the refusals that authorises this:

if errors.Is(err, forge.ErrUnauthorized) {
    p.Invalidate(ep)
}

forge.ErrForbidden and forge.ErrRateLimited must NOT trigger it, for the reasons in the table above.

It does not dispose the discarded provider: a component may still hold one it obtained earlier and be mid-operation with it, and nothing here can know. Providers in this module hold no resource that requires closing.

Invalidating an endpoint never asked for is a no-op.

func (*Pool) Provider

func (p *Pool) Provider(ctx context.Context, ep forge.Endpoint) (forge.Provider, error)

Provider returns the provider for ep, building it at most once.

A convenience over Pool.Source followed by Source.Provider, for a caller that holds the pool anyway.

func (*Pool) Resolve

func (p *Pool) Resolve(ctx context.Context, eps ...forge.Endpoint) error

Resolve builds the providers for eps now, returning the first failure.

It exists so a command can fail at its own boundary. Resolution is otherwise lazy, which moves a persistent misconfiguration — an unset credential, an unreachable host — from start-up to first use: an operator running several operations sees it fail one operation in, from something that appeared to have started cleanly. Because no failure is held, the same misconfiguration then re-surfaces at every first use.

Nothing requires it, and the lazy path is unchanged for callers that prefer resolution to happen when the work does.

func (*Pool) Source

func (p *Pool) Source(ep forge.Endpoint) *Source

Source returns the accessor for one endpoint, building nothing.

This is what a component should be given — not the pool, and not a resolved provider.

Handing over a resolved provider would mean the wiring code chose when resolution happens, and therefore when it fails, for every component at once. An accessor lets each decide. Handing over the pool would give a component reach over endpoints that are not its business.

A component should declare the single-method interface it needs rather than importing this package for a type:

type providerSource interface {
    Provider(ctx context.Context) (forge.Provider, error)
}

A component with no pool to hand can then be given a closure over forge.Lookup satisfying the same interface, so zero-conf stays a fallback rather than a second code path through the component.

It is pure and cannot fail: an endpoint that cannot be served fails at the first Source.Provider, where a caller has a context to bound it and an error to receive.

type Source

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

Source hands out the provider for one endpoint.

A component depends on this rather than on Pool: it can reach exactly the endpoint it was given and nothing else.

func (*Source) Endpoint

func (s *Source) Endpoint() forge.Endpoint

Endpoint reports which endpoint this source serves, for diagnostics.

func (*Source) Provider

func (s *Source) Provider(ctx context.Context) (forge.Provider, error)

Provider returns the provider, building it on first use and reusing it after.

Concurrent callers share one build. A caller whose own context ends returns promptly without cancelling that shared build or affecting the callers still waiting. A failed build is never held: the next call starts a new attempt rather than being handed the old failure.

Jump to

Keyboard shortcuts

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