Skip to content

polygon/heimdall: collapse V1/V2 fetch scaffold into fetchVersioned generic#22167

Closed
yperbasis wants to merge 2 commits into
mainfrom
yperbasis/heimdall-fetch-versioned
Closed

polygon/heimdall: collapse V1/V2 fetch scaffold into fetchVersioned generic#22167
yperbasis wants to merge 2 commits into
mainfrom
yperbasis/heimdall-fetch-versioned

Conversation

@yperbasis

Copy link
Copy Markdown
Member

Every fetcher in client_http.go repeated the same scaffold: if c.Version() == HeimdallV2 { FetchWithRetry[V2] → transform } else { FetchWithRetry[V1] → &resp.Result }. This collapses the nine fetchers onto fetchVersioned[TV1, TV2, T] (plus an opts variant carrying isRecoverableError for FetchMilestone and wrapFetchErr for FetchSpan's spanID=%d wrapping, which applies to fetch errors but not transform errors — preserved exactly).

URL construction (paths, pagination, latest) stays at call sites; V1/V2 response structs untouched; V2 count strconv.Atoi semantics kept.

Before the refactor, two table-driven test functions pin current behavior (24 subtests: all 9 fetchers × V1/V2 routing/paths/pagination/transforms, spanID error wrapping, ErrNotInMilestoneList mapping, and retry counts proving the recoverable-error threading) — green against the old code first, green after.

Considered and rejected: converting poshttp.FetchWithRetryEx to cenkalti/backoff/v4 — backoff's Notify never fires on the final attempt and bridging the client's closeCh would need a per-call goroutine plus error disambiguation that re-implements the current wait; details in the refactor commit body.

Net −41 lines in production code (~300 → ~200 at call sites) plus +429 of pinning tests. Verified: go test ./polygon/..., scoped golangci-lint clean (×2), make erigon and make integration.

Part of a dedup series; sibling fixes in #22165, #22166.

yperbasis added 2 commits July 2, 2026 21:20
Cover all nine versioned fetchers in both Heimdall API versions via the
mocked HTTP request handler: URL paths and pagination queries, response
transforms, count string parsing, fetch error wrapping, invalid milestone
index mapping to ErrNotInMilestoneList, and recoverable-error retry
behavior. Safety net for collapsing the repeated V1/V2 fetch scaffold.
…eneric

Every versioned fetcher repeated the same scaffold: switch on
c.Version(), fetch the V1 or V2 response type with retry, then apply the
per-version transform. Introduce fetchVersioned/fetchVersionedEx generics
that own that flow, and convert all nine fetchers to them. URL
construction stays at call sites since paths and pagination queries
differ per endpoint and version.

fetchVersionedOpts carries the two per-site knobs: isRecoverableError
(FetchMilestone's pruned-milestone retry decision) and wrapFetchErr,
which wraps only fetch errors so FetchSpan's spanID context and
FetchMilestone's ErrNotInMilestoneList mapping keep applying exactly
where they did before, leaving transform errors untouched. V2 string
counts keep strconv.Atoi semantics via parseCount.

Both version URLs are now built eagerly; MakeURL can only fail on
parsing the base URL string, which is version-independent, so the error
surface is unchanged.

A follow-up idea to rewrite poshttp.FetchWithRetryEx on top of
cenkalti/backoff was considered and dropped: bridging client.closeCh into
a context plus reproducing the select's priority between ctx.Err(),
ErrShutdownDetected and the last attempt error (which can itself wrap
context.Canceled) would need a per-call goroutine and post-hoc error
disambiguation, and backoff's Notify hook never fires on the final
attempt, changing the attempt-log cadence.
@yperbasis yperbasis requested a review from AskAlexSharov as a code owner July 2, 2026 19:30
@yperbasis yperbasis closed this Jul 2, 2026
Sahil-4555 pushed a commit to Sahil-4555/erigon that referenced this pull request Jul 3, 2026
…ontech#22168)

The five `GetOrCreate{Histogram,Counter,Gauge,SummaryExt,GaugeVec}`
methods repeated the same ~32-line double-checked-locking block; each
copy differed only in the constructor and type assertion. They collapse
onto `getOrCreate[T prometheus.Metric]` / `getOrCreateVec[T
prometheus.Collector]`, and each method is now a 3-liner.

The GaugeVec copy is where clone mutation had gone wrong: its type guard
compared `reflect.TypeFor[*prometheus.GaugeVec]()` to itself — always
false, i.e. dead (and unreachable while `namedMetricVec.metric` was
concretely typed `*prometheus.GaugeVec`). The generic widens the field
to `prometheus.Collector` and performs a real assertion with the same
error shape as the scalar family
(`TestSetGetOrCreateGaugeVecWrongType`). `registerMetricVec` is removed:
its re-check-and-insert was a duplicate of the inline double-checked
insert.

First commit adds characterization tests (+127 lines) pinning existing
GetOrCreate* behavior — same-instance returns, wrong-type errors,
describe-once semantics — green against the old code before the
refactor.

**Update (third commit):** `getOrCreate`/`getOrCreateVec` were
themselves clones differing only in which registry (`m`/`a` vs
`vecs`/`av`) they touched. They now delegate to a single `getOrCreateIn`
over a shared `namedEntry[M]` entry type (`namedMetric`/`namedMetricVec`
become type aliases), with the wrappers widening `T` through an adapter
closure so the store stays compile-time safe. The double-checked locking
becomes one critical section: the fast path took the same mutex anyway,
and the create funcs are pure `parseMetric` + constructor calls, so
creating under the lock eliminates the create-twice-drop-loser race. The
unlock is deferred because prometheus constructors can panic on inputs
reachable through the public API (a `quantile`/`le` const label, a
negative summary window); a non-deferred unlock would strand the set's
mutex. Also drops the dead scalar `namedMetric.isAux` — never assigned,
so the `ListMetricNames` filter on it was unreachable.

Net −169 lines in set.go. Verified: package tests incl. `-race`, `make
lint` clean, full `go build ./...`.

Part of a dedup series; siblings erigontech#22165, erigontech#22166, erigontech#22167.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant