feat(forgejo-metrics): adjust line stats caching strategy and improve retry logic for contributor stats
This commit is contained in:
parent
c04ab6ac8b
commit
dd9681925e
|
|
@ -36,7 +36,8 @@ if TYPE_CHECKING:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_line_stats_cache: dict[str, tuple[float, int, int, bool]] = {}
|
_line_stats_cache: dict[str, tuple[float, int, int, bool]] = {}
|
||||||
_line_stats_fetching: set[str] = set()
|
_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(
|
async def _bg_fetch_line_stats(
|
||||||
|
|
@ -383,8 +384,9 @@ async def get_forgejo_heatmap(
|
||||||
cached = _line_stats_cache.get(cache_key)
|
cached = _line_stats_cache.get(cache_key)
|
||||||
now = _time.monotonic()
|
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 (
|
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,
|
# Normalise base_url the same way get_forgejo_client() does, eagerly,
|
||||||
# so the background task never touches a potentially-closed session.
|
# so the background task never touches a potentially-closed session.
|
||||||
|
|
|
||||||
|
|
@ -353,28 +353,22 @@ class ForgejoAPIClient:
|
||||||
async def get_contributor_stats(self, owner: str, repo: str) -> tuple[list[dict], bool]:
|
async def get_contributor_stats(self, owner: str, repo: str) -> tuple[list[dict], bool]:
|
||||||
"""Fetch per-contributor weekly stats for a repository.
|
"""Fetch per-contributor weekly stats for a repository.
|
||||||
|
|
||||||
Returns (contributors, has_data). On the first call Forgejo may return
|
Returns (contributors, has_data). Forgejo returns HTTP 202 while it
|
||||||
HTTP 202 ("computing") — we wait 2 s and retry once so the stats are
|
computes stats — that is not an error, it just means the caller should
|
||||||
available on the next dashboard load even if not this one.
|
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
|
Each contributor dict has a ``weeks`` array with ``w`` (Unix timestamp),
|
||||||
week start), ``a`` (additions), and ``d`` (deletions).
|
``a`` (additions), and ``d`` (deletions).
|
||||||
"""
|
"""
|
||||||
import asyncio as _asyncio
|
|
||||||
|
|
||||||
client = await self._get_client()
|
client = await self._get_client()
|
||||||
url = f"/api/v1/repos/{owner}/{repo}/stats/contributors"
|
response = await client.get(
|
||||||
response = await client.get(url)
|
f"/api/v1/repos/{owner}/{repo}/stats/contributors"
|
||||||
|
)
|
||||||
if response.status_code == 202:
|
if response.status_code == 202:
|
||||||
# Forgejo is computing — wait briefly then try once more
|
return [], False # still computing — caller will retry shortly
|
||||||
await _asyncio.sleep(2)
|
|
||||||
response = await client.get(url)
|
|
||||||
|
|
||||||
if response.status_code == 202:
|
|
||||||
return [], False # still computing after retry
|
|
||||||
if response.status_code == 404:
|
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()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
return (data if isinstance(data, list) else []), True
|
return (data if isinstance(data, list) else []), True
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,12 @@ interface ForgejoHeatmapProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CELL = 13;
|
const CELL = 16;
|
||||||
const GAP = 3;
|
const GAP = 4;
|
||||||
const STRIDE = CELL + GAP;
|
const STRIDE = CELL + GAP;
|
||||||
const WEEKS = 27;
|
const WEEKS = 27;
|
||||||
const LEFT = 28;
|
const LEFT = 32;
|
||||||
const TOP = 18;
|
const TOP = 20;
|
||||||
|
|
||||||
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
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 VIOLET = "rgba(139,92,246,1)";
|
||||||
const GREEN = "rgba(52,211,153,1)";
|
const GREEN = "rgba(52,211,153,1)";
|
||||||
const RED = "rgba(248,113,113,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 {
|
function toLevel(count: number): number {
|
||||||
if (count === 0) return 0;
|
if (count === 0) return 0;
|
||||||
|
|
@ -166,23 +166,15 @@ export function ForgejoHeatmap({
|
||||||
<span>More</span>
|
<span>More</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Contributions badge ──────────────────────────────────── */}
|
{/* ── Contributions summary ────────────────────────────────── */}
|
||||||
<div className="flex justify-center">
|
<p className="text-center">
|
||||||
<div
|
<span className="text-2xl font-bold tabular-nums" style={{ color: VIOLET }}>
|
||||||
className="inline-flex items-baseline gap-2 rounded-full px-5 py-2"
|
{totalEvents.toLocaleString()}
|
||||||
style={{
|
</span>
|
||||||
background: "rgba(139,92,246,0.10)",
|
<span className="ml-1.5 text-sm" style={{ color: "rgba(255,255,255,0.70)" }}>
|
||||||
border: "1px solid rgba(139,92,246,0.25)",
|
contributions across all tracked repositories in the last 6 months
|
||||||
}}
|
</span>
|
||||||
>
|
</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* ── Line stats ──────────────────────────────────────────── */}
|
{/* ── Line stats ──────────────────────────────────────────── */}
|
||||||
{hasLineStats ? (
|
{hasLineStats ? (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue