Skip to content

feat(registry): fetch project data, limits and features concurrently - #35

Open
geekbrother wants to merge 1 commit into
mainfrom
feat/parallel-project-data-fetch
Open

feat(registry): fetch project data, limits and features concurrently#35
geekbrother wants to merge 1 commit into
mainfrom
feat/parallel-project-data-fetch

Conversation

@geekbrother

Copy link
Copy Markdown
Member

Summary

project_data_with awaited its upstream calls in sequence, so one call cost the sum of two round trips (include_limits — nearly every caller) or three (include_limits + include_features). The calls are independent and span two different hosts — the explorer serves /internal/project/key/{id}, the internal API serves /internal/v1/project-limits and /appkit/v1/config — so there was never a reason to chain them.

This issues all three concurrently with tokio::join!. Wall time becomes the slowest single call rather than their sum.

Context: blockchain-api pays this on every cache miss, and its registry fetch latency sits at ~200ms baseline with spikes to the 800ms client timeout. Serialised round trips are a large part of that, and the additive tail means the combined p99 is worse than either upstream's.

Why join! and not try_join!

try_join! returns the first error it sees. A transport or 5xx failure on limits/features would then pre-empt a 404 on the base project data and report "registry unavailable" where the sequential code reported "project not found". Callers fail open on a registry error, so that would admit unregistered projects — the fail-open bug #33 and #34 fixed.

Awaiting all three and resolving them in the original precedence order keeps the outcome identical to the sequential version for every input; only timing changes.

This isn't hypothetical — I built the try_join! variant and ran the new test against it:

a 404 on the base project data must surface as Ok(None), not a registry error
that makes callers fail open; got ServerError("status=500 ...")

Trade-off

A project that 404s now also issues the limits/features calls the sequential version skipped. Unknown project IDs are a negligible share of traffic and callers cache the not-found result, so this trades a rare extra call for a latency win on the common path. Documented in the code comment.

Test plan

Two tests added, both verified to fail without the change:

  • project_data_with_issues_upstream_calls_concurrently — three endpoints each delayed 300ms; asserts the call completes in <600ms. Measured 928ms against sequential code, ~300ms concurrent.
  • project_data_with_reports_not_found_even_when_sub_resource_fails — base 404 + limits 500 must yield Ok(None). Fails with ServerError under try_join!, passes with join!.
  • Full suite: 32 passed, 0 failed.
  • cargo clippy --all-features --all-targets clean; cargo fmt touched only the changed regions.

Adds the macros tokio feature for join!.

Downstream note

blockchain-api pins cerberus at v0.16.1, so this needs a new tag before it can be picked up there. Left as a release decision rather than assumed.

🤖 Generated with Claude Code

`project_data_with` awaited its upstream calls in sequence, so a single
call cost the sum of two round trips (`include_limits`, which is nearly
every caller) or three (`include_limits` + `include_features`). The calls
are independent and span two hosts — the explorer serves project data,
the internal API serves limits and features — so there was never a reason
to chain them.

Issue all three concurrently with `tokio::join!`. Wall time becomes the
slowest single call instead of their sum, which matters because callers
pay this on every cache miss.

`join!` rather than `try_join!`, deliberately: `try_join!` returns the
first error it sees, so a transport or 5xx failure on limits/features
would pre-empt a 404 on the base project data and report "registry
unavailable" where the sequential code reported "project not found".
Callers fail open on a registry error, so that regression would admit
unregistered projects — the bug #33 and #34 fixed. Awaiting all three and
then resolving them in the original precedence order keeps the outcome
identical to the sequential version for every input; only timing changes.

The one behavioural trade: a project that 404s now also issues the
limits/features calls the sequential version skipped. Unknown project IDs
are a negligible share of traffic and callers cache the not-found result,
so this buys a latency win on the common path for a rare extra call.

Both new tests were verified to fail without the change:
- the concurrency test measures 928ms against sequential code and ~300ms
  concurrent, with the bound at 600ms
- the not-found test reports `ServerError` under a `try_join!`
  implementation, and passes with `join!`

Adds the `macros` tokio feature for `join!`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@geekbrother geekbrother self-assigned this Jul 31, 2026
@geekbrother geekbrother changed the title perf(registry): fetch project data, limits and features concurrently feat(registry): fetch project data, limits and features concurrently Jul 31, 2026
@geekbrother
geekbrother marked this pull request as ready for review July 31, 2026 09:25
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.

2 participants