feat(forgejo-metrics): adjust line stats caching strategy and improve retry logic for contributor stats

This commit is contained in:
null 2026-05-22 16:19:15 -05:00
parent c04ab6ac8b
commit dd9681925e
3 changed files with 29 additions and 41 deletions

View File

@ -36,7 +36,8 @@ if TYPE_CHECKING:
# ---------------------------------------------------------------------------
_line_stats_cache: dict[str, tuple[float, int, int, bool]] = {}
_line_stats_fetching: set[str] = set()
_LINE_STATS_TTL = 300 # seconds before a re-fetch is triggered
_LINE_STATS_TTL_HIT = 300 # 5 min — re-fetch cadence once real data is cached
_LINE_STATS_TTL_MISS = 30 # 30 s — retry cadence while Forgejo is still computing
async def _bg_fetch_line_stats(
@ -383,8 +384,9 @@ async def get_forgejo_heatmap(
cached = _line_stats_cache.get(cache_key)
now = _time.monotonic()
ttl = _LINE_STATS_TTL_HIT if (cached and cached[3]) else _LINE_STATS_TTL_MISS
if cache_key not in _line_stats_fetching and (
cached is None or now - cached[0] > _LINE_STATS_TTL
cached is None or now - cached[0] > ttl
):
# Normalise base_url the same way get_forgejo_client() does, eagerly,
# so the background task never touches a potentially-closed session.

View File

@ -353,28 +353,22 @@ class ForgejoAPIClient:
async def get_contributor_stats(self, owner: str, repo: str) -> tuple[list[dict], bool]:
"""Fetch per-contributor weekly stats for a repository.
Returns (contributors, has_data). On the first call Forgejo may return
HTTP 202 ("computing") we wait 2 s and retry once so the stats are
available on the next dashboard load even if not this one.
Returns (contributors, has_data). Forgejo returns HTTP 202 while it
computes stats that is not an error, it just means the caller should
retry later. has_data=False signals the 202 case; the application layer
uses a short retry TTL so it probes again within ~30 s.
Each contributor has a ``weeks`` array with ``w`` (Unix timestamp of
week start), ``a`` (additions), and ``d`` (deletions).
Each contributor dict has a ``weeks`` array with ``w`` (Unix timestamp),
``a`` (additions), and ``d`` (deletions).
"""
import asyncio as _asyncio
client = await self._get_client()
url = f"/api/v1/repos/{owner}/{repo}/stats/contributors"
response = await client.get(url)
response = await client.get(
f"/api/v1/repos/{owner}/{repo}/stats/contributors"
)
if response.status_code == 202:
# Forgejo is computing — wait briefly then try once more
await _asyncio.sleep(2)
response = await client.get(url)
if response.status_code == 202:
return [], False # still computing after retry
return [], False # still computing — caller will retry shortly
if response.status_code == 404:
return [], True # no data, but not a 202 — treat as "has_data"
return [], True # repo exists but no commits; treat as data-present
response.raise_for_status()
data = response.json()
return (data if isinstance(data, list) else []), True

View File

@ -12,12 +12,12 @@ interface ForgejoHeatmapProps {
isLoading?: boolean;
}
const CELL = 13;
const GAP = 3;
const CELL = 16;
const GAP = 4;
const STRIDE = CELL + GAP;
const WEEKS = 27;
const LEFT = 28;
const TOP = 18;
const LEFT = 32;
const TOP = 20;
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
@ -33,7 +33,7 @@ const LEVEL_FILL = [
const VIOLET = "rgba(139,92,246,1)";
const GREEN = "rgba(52,211,153,1)";
const RED = "rgba(248,113,113,1)";
const LABEL_CLR = "rgba(139,92,246,0.65)";
const LABEL_CLR = "rgba(255,255,255,0.70)"; // white labels
function toLevel(count: number): number {
if (count === 0) return 0;
@ -166,23 +166,15 @@ export function ForgejoHeatmap({
<span>More</span>
</div>
{/* ── Contributions badge ──────────────────────────────────── */}
<div className="flex justify-center">
<div
className="inline-flex items-baseline gap-2 rounded-full px-5 py-2"
style={{
background: "rgba(139,92,246,0.10)",
border: "1px solid rgba(139,92,246,0.25)",
}}
>
<span className="text-2xl font-bold tabular-nums" style={{ color: VIOLET }}>
{totalEvents.toLocaleString()}
</span>
<span className="text-sm font-medium" style={{ color: LABEL_CLR }}>
contributions across all tracked repositories in the last 6 months
</span>
</div>
</div>
{/* ── Contributions summary ────────────────────────────────── */}
<p className="text-center">
<span className="text-2xl font-bold tabular-nums" style={{ color: VIOLET }}>
{totalEvents.toLocaleString()}
</span>
<span className="ml-1.5 text-sm" style={{ color: "rgba(255,255,255,0.70)" }}>
contributions across all tracked repositories in the last 6 months
</span>
</p>
{/* ── Line stats ──────────────────────────────────────────── */}
{hasLineStats ? (