Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions components/OrchestratingView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => {
const maxScore = useMemo(() => {
const topTransData = Object.keys(scores?.scores ?? {}).reduce(
(prev, curr) => {
const score = scores?.scores[curr];
const score = scores?.scores?.[curr];
const region =
knownRegions?.regions?.find((r) => r.id === curr)?.name ?? "N/A";
if (
Expand All @@ -107,7 +107,7 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => {
) {
return {
region: region,
score: scores?.scores[curr],
score,
};
}
return prev;
Expand All @@ -128,8 +128,9 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => {
precision: 1,
})} - ${maxScore.transcoding.region}`
: "";
return outputTrans ? transcodingInfo : "N/A";
}, [maxScore]);
if (outputTrans) return transcodingInfo;
return scores?.scores === null ? "Unavailable" : "N/A";
}, [maxScore, scores]);

const maxAIScoreOutput = useMemo(() => {
const outputAI = maxScore.ai?.value && maxScore.ai?.value > 0;
Expand All @@ -148,8 +149,11 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => {
score: aiInfo,
modelText: `. The pipeline and model for this Orchestrator was '${maxScore.ai?.pipeline}' and '${maxScore.ai?.model}'`,
}
: { score: "N/A", modelText: "" };
}, [knownRegions?.regions, maxScore]);
: {
score: scores?.topAIScore === null ? "Unavailable" : "N/A",
modelText: "",
};
}, [knownRegions?.regions, maxScore, scores]);

return (
<Box
Expand Down Expand Up @@ -223,7 +227,13 @@ const Index = ({ currentRound, transcoder, isActive }: Props) => {
className="masonry-grid_item"
label="Price / Pixel"
tooltip="The most recent price for transcoding which the orchestrator is currently advertising off-chain to gateways. This may be different from on-chain pricing."
value={scores ? `${formatNumber(scores.pricePerPixel)} WEI` : "N/A"}
value={
!scores
? "N/A"
: scores.pricePerPixel === null
? "Unavailable"
: `${formatNumber(scores.pricePerPixel)} WEI`
}
/>
{/* <Stat
className="masonry-grid_item"
Expand Down
11 changes: 6 additions & 5 deletions lib/api/types/get-performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ export type Score = {
orchestrator: string;
};

// Null means the upstream could not be reached; empty or zero means no data.
export type PerformanceMetrics = {
successRates: RegionalValues;
roundTripScores: RegionalValues;
successRates: RegionalValues | null;
roundTripScores: RegionalValues | null;

scores: RegionalValues;
scores: RegionalValues | null;

pricePerPixel: number;
pricePerPixel: number | null;

topAIScore: Score;
topAIScore: Score | null;
};

export type AllPerformanceMetrics = {
Expand Down
105 changes: 53 additions & 52 deletions pages/api/score/[address].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {
PerformanceMetrics,
RegionalValues,
Score,
} from "@lib/api/types/get-performance";
import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "@lib/chains";
import { fetchWithRetry } from "@lib/fetchWithRetry";
Expand All @@ -30,14 +31,6 @@ export type MetricsResponse = {
| undefined;
};

type ScoreResponse = {
value: number;
region: string;
model: string;
pipeline: string;
orchestrator: string;
};

export type PriceResponse = {
Address: string;
ServiceURI: string;
Expand All @@ -53,6 +46,34 @@ export type PriceResponse = {
UpdatedAt: number;
}[];

/**
* Fetch and parse JSON from a given URL.
* @returns The parsed JSON, or null if the fetch fails. Never rejects, so one
* failed upstream cannot reject a Promise.all.
*/
const fetchJson = async <T,>(url: string): Promise<T | null> => {
try {
const response = await fetchWithRetry(url);

if (!response.ok) {
console.error(
`Fetch error: ${url}`,
response.status,
(await response.text()).slice(0, 500)
);
return null;
}

return await response.json();
} catch (err) {
console.error(
`Fetch error: ${url}`,
err instanceof Error ? err.message : err
);
return null;
}
};

const handler = async (
req: NextApiRequest,
res: NextApiResponse<PerformanceMetrics | null>
Expand All @@ -63,63 +84,39 @@ const handler = async (
if (method === "GET") {
const { address } = req.query;

res.setHeader("Cache-Control", getCacheControlHeader("hour"));

if (!!address && !Array.isArray(address) && isAddress(address)) {
const transcoderId = address.toLowerCase();

const topScoreUrl = `${process.env.NEXT_PUBLIC_AI_METRICS_SERVER_URL}/api/top_ai_score?orchestrator=${transcoderId}`;
const metricsUrl = `${process.env.NEXT_PUBLIC_METRICS_SERVER_URL}/api/aggregated_stats?orchestrator=${transcoderId}`;
const pricingUrl = `${CHAIN_INFO[DEFAULT_CHAIN_ID].pricingUrl}?excludeUnavailable=False`;

const [topScoreResponse, metricsResponse, priceResponse] =
await Promise.all([
fetchWithRetry(topScoreUrl),
fetchWithRetry(metricsUrl),
fetchWithRetry(pricingUrl),
]);

if (!topScoreResponse.ok) {
const errorText = await topScoreResponse.text();
console.error(
"Top AI score fetch error:",
topScoreResponse.status,
errorText
);
return externalApiError(res, "AI metrics server");
}
const [topAIScore, metrics, transcodersWithPrice] = await Promise.all([
fetchJson<Score>(topScoreUrl),
fetchJson<MetricsResponse>(metricsUrl),
fetchJson<PriceResponse>(pricingUrl),
]);

if (!metricsResponse.ok) {
const errorText = await metricsResponse.text();
console.error(
"Metrics fetch error:",
metricsResponse.status,
errorText
);
return externalApiError(res, "metrics server");
// Every upstream being down is never a legitimate empty state, so fail
// loudly rather than rendering a healthy-looking orchestrator with no data.
if (!topAIScore && !metrics && !transcodersWithPrice) {
return externalApiError(res, "all metrics servers");
}

if (!priceResponse.ok) {
const errorText = await priceResponse.text();
console.error(
"Transcoder price fetch error:",
priceResponse.status,
errorText
);
return externalApiError(res, "pricing server");
}

const topAIScore: ScoreResponse = await topScoreResponse.json();
const metrics: MetricsResponse = await metricsResponse.json();
const transcodersWithPrice: PriceResponse = await priceResponse.json();
// Expire quickly so upstream recovery is not hidden for an hour.
const degraded = !topAIScore || !metrics || !transcodersWithPrice;
res.setHeader(
"Cache-Control",
getCacheControlHeader(degraded ? "minute" : "hour")
);

const transcoderWithPrice = transcodersWithPrice.find((t) =>
const transcoderWithPrice = transcodersWithPrice?.find((t) =>
checkAddressEquality(t.Address, transcoderId)
);

const uniqueRegions = (() => {
const keys = new Set<string>();
Object.values(metrics).forEach((metric) => {
Object.values(metrics ?? {}).forEach((metric) => {
if (metric) {
Object.keys(metric).forEach((key) => keys.add(key));
}
Expand All @@ -130,8 +127,10 @@ const handler = async (
const createMetricsObject = (
metricKey: keyof Metric,
transcoderId: string,
metrics: MetricsResponse
): RegionalValues => {
metrics: MetricsResponse | null
): RegionalValues | null => {
if (!metrics) return null;

const metricsObject: RegionalValues = uniqueRegions.reduce(
(acc, metricsRegionKey) => {
const value =
Expand All @@ -153,7 +152,9 @@ const handler = async (
};

const combined: PerformanceMetrics = {
pricePerPixel: transcoderWithPrice?.PricePerPixel ?? 0,
pricePerPixel: transcodersWithPrice
? transcoderWithPrice?.PricePerPixel ?? 0
: null,
successRates: createMetricsObject(
"success_rate",
transcoderId,
Expand Down
Loading