Skip to content

Commit 7a31a21

Browse files
authored
fix: Reduce Solana data dashboard Databricks traffic (#1602)
* fix: update data fetching logic to remove unnecessary parameters and improve cache handling * fix: simplify cache key generation by removing unused refresh logic * fix: improve error handling in in-memory Databricks data caching
1 parent daa9869 commit 7a31a21

2 files changed

Lines changed: 57 additions & 138 deletions

File tree

apps/web/src/app/[locale]/data/solana-data-dashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1664,7 +1664,7 @@ function aggregate(sum: number, count: number, aggregation: Aggregation) {
16641664
}
16651665

16661666
async function fetchData(url: string, errorMessages: DataFetchErrorMessages) {
1667-
const response = await fetch(url, { cache: "no-store" });
1667+
const response = await fetch(url);
16681668
const payload = await readDataPayload(response, errorMessages);
16691669

16701670
if (!response.ok) {

apps/web/src/app/api/databricks/data/route.ts

Lines changed: 56 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@ export const dynamic = "force-dynamic";
2020
export const revalidate = 0;
2121

2222
const DATABRICKS_CACHE_REVALIDATE_SECONDS = 12 * 60 * 60;
23-
const BROWSER_CACHE_SECONDS = 60;
2423
const EDGE_STALE_SECONDS = 24 * 60 * 60;
2524
const DEFAULT_RANGE_DAYS = 90;
25+
const MAX_RANGE_DAYS = Math.max(...rangeOptions.map((option) => option.value));
26+
const DATABRICKS_CACHE_KEY_VERSION = "solana-data-databricks-metric-rows-v4";
2627
const IS_PRODUCTION = isProduction();
2728
const NO_STORE_CACHE_CONTROL = "no-store, max-age=0";
2829
const DAY_IN_MS = 24 * 60 * 60 * 1000;
29-
const DATA_REFRESH_TIME_ZONE = "America/New_York";
30-
// Source jobs run around 9 AM and 9 PM Eastern; roll dashboard caches an hour later.
31-
const DATA_REFRESH_HOURS = [10, 22] as const;
3230
const DATA_UNAVAILABLE_ERROR =
3331
"Solana data is unavailable right now. Try again in a moment.";
3432

@@ -56,7 +54,7 @@ export async function GET(request: NextRequest) {
5654
const rangeDays = parseRangeDays(request.nextUrl.searchParams.get("days"));
5755

5856
try {
59-
const result = await getDatabricksData(configResult.config, rangeDays);
57+
const result = await getDatabricksData(configResult.config);
6058

6159
return json<DataApiResponse>(
6260
buildDataResponse(result, rangeDays),
@@ -105,33 +103,29 @@ function filterRowsByRange(rows: MetricRow[], rangeDays: number) {
105103
});
106104
}
107105

108-
function getDatabricksData(config: DatabricksConfig, rangeDays: number) {
106+
function getDatabricksData(config: DatabricksConfig) {
109107
return IS_PRODUCTION
110-
? getCachedDatabricksData(config, rangeDays)
111-
: fetchDatabricksData(config, rangeDays);
108+
? getCachedDatabricksData(config)
109+
: fetchDatabricksData(config);
112110
}
113111

114-
function getCachedDatabricksData(config: DatabricksConfig, rangeDays: number) {
112+
function getCachedDatabricksData(config: DatabricksConfig) {
113+
const dataCacheKey = getDatabricksCacheKey(config);
114+
const cacheKeyParts = [DATABRICKS_CACHE_KEY_VERSION, dataCacheKey];
115+
115116
return unstable_cache(
116-
() => fetchDatabricksData(config, rangeDays),
117-
[
118-
"solana-data-databricks-metric-rows-v3",
119-
getDatabricksRefreshCacheKey(),
120-
getDatabricksCacheKey(config, rangeDays),
121-
],
117+
() => getInMemoryCachedDatabricksData(config, cacheKeyParts.join("|")),
118+
cacheKeyParts,
122119
{
123120
revalidate: DATABRICKS_CACHE_REVALIDATE_SECONDS,
124121
tags: ["solana-data-databricks"],
125122
},
126123
)();
127124
}
128125

129-
async function fetchDatabricksData(
130-
config: DatabricksConfig,
131-
rangeDays: number,
132-
) {
126+
async function fetchDatabricksData(config: DatabricksConfig) {
133127
const result = await getDatabricksSqlMetricRows(config, {
134-
lookbackDays: rangeDays,
128+
lookbackDays: MAX_RANGE_DAYS,
135129
metricNames,
136130
});
137131

@@ -141,139 +135,64 @@ async function fetchDatabricksData(
141135
};
142136
}
143137

144-
function getDatabricksCacheKey(config: DatabricksConfig, rangeDays: number) {
145-
return [
146-
config.serverHostname,
147-
config.httpPath,
148-
config.warehouseId,
149-
rangeDays,
150-
metricNames.join(","),
151-
].join("|");
152-
}
153-
154-
function getDatabricksRefreshCacheKey(now = new Date()) {
155-
const parts = getTimeZoneDateTimeParts(now, DATA_REFRESH_TIME_ZONE);
156-
const latestRefreshHour = getLatestScheduledRefreshHour(parts.hour);
157-
const refreshHour =
158-
latestRefreshHour ?? DATA_REFRESH_HOURS[DATA_REFRESH_HOURS.length - 1];
159-
const refreshDate = new Date(
160-
Date.UTC(
161-
parts.year,
162-
parts.month - 1,
163-
parts.day + (latestRefreshHour === undefined ? -1 : 0),
164-
refreshHour,
165-
),
166-
);
138+
const databricksDataRequests = new Map<
139+
string,
140+
Promise<Awaited<ReturnType<typeof fetchDatabricksData>>>
141+
>();
167142

168-
return `${refreshDate.toISOString().slice(0, 10)}-${String(refreshHour).padStart(2, "0")}`;
169-
}
143+
function getInMemoryCachedDatabricksData(
144+
config: DatabricksConfig,
145+
cacheKey: string,
146+
) {
147+
const cachedRequest = databricksDataRequests.get(cacheKey);
170148

171-
function getSuccessCacheControl(now = new Date()) {
172-
if (!IS_PRODUCTION) {
173-
return NO_STORE_CACHE_CONTROL;
149+
if (cachedRequest) {
150+
return cachedRequest;
174151
}
175152

176-
return [
177-
"public",
178-
`max-age=${BROWSER_CACHE_SECONDS}`,
179-
`s-maxage=${getSecondsUntilNextScheduledRefresh(now)}`,
180-
`stale-while-revalidate=${EDGE_STALE_SECONDS}`,
181-
].join(", ");
182-
}
153+
pruneDatabricksDataRequests(cacheKey);
183154

184-
function getSecondsUntilNextScheduledRefresh(now = new Date()) {
185-
const parts = getTimeZoneDateTimeParts(now, DATA_REFRESH_TIME_ZONE);
186-
const nextRefresh = getScheduledRefreshDate(parts, now);
187-
const seconds = Math.ceil((nextRefresh.getTime() - now.getTime()) / 1000);
155+
const request = fetchDatabricksData(config);
188156

189-
return Math.max(1, seconds);
190-
}
191-
192-
function getScheduledRefreshDate(
193-
parts: ReturnType<typeof getTimeZoneDateTimeParts>,
194-
now: Date,
195-
) {
196-
const nextRefreshHour = getNextScheduledRefreshHour(parts.hour);
197-
const refreshHour = nextRefreshHour ?? DATA_REFRESH_HOURS[0];
198-
const dayOffset = nextRefreshHour === undefined ? 1 : 0;
199-
200-
return zonedTimeToUtc(
201-
parts.year,
202-
parts.month,
203-
parts.day + dayOffset,
204-
refreshHour,
205-
DATA_REFRESH_TIME_ZONE,
206-
now,
157+
request.then(
158+
() => databricksDataRequests.delete(cacheKey),
159+
() => databricksDataRequests.delete(cacheKey),
207160
);
208-
}
209161

210-
function getLatestScheduledRefreshHour(hour: number) {
211-
return DATA_REFRESH_HOURS.findLast((refreshHour) => hour >= refreshHour);
212-
}
162+
databricksDataRequests.set(cacheKey, request);
213163

214-
function getNextScheduledRefreshHour(hour: number) {
215-
return DATA_REFRESH_HOURS.find((refreshHour) => hour < refreshHour);
164+
return request;
216165
}
217166

218-
function zonedTimeToUtc(
219-
year: number,
220-
month: number,
221-
day: number,
222-
hour: number,
223-
timeZone: string,
224-
referenceDate: Date,
225-
) {
226-
const utcGuess = new Date(Date.UTC(year, month - 1, day, hour));
227-
const firstPass = new Date(
228-
utcGuess.getTime() - getTimeZoneOffsetMs(utcGuess, timeZone),
229-
);
230-
const secondPass = new Date(
231-
utcGuess.getTime() - getTimeZoneOffsetMs(firstPass, timeZone),
232-
);
233-
234-
return Math.abs(secondPass.getTime() - referenceDate.getTime()) <
235-
DAY_IN_MS * 2
236-
? secondPass
237-
: firstPass;
167+
function pruneDatabricksDataRequests(activeCacheKey: string) {
168+
for (const cacheKey of databricksDataRequests.keys()) {
169+
if (cacheKey !== activeCacheKey) {
170+
databricksDataRequests.delete(cacheKey);
171+
}
172+
}
238173
}
239174

240-
function getTimeZoneOffsetMs(date: Date, timeZone: string) {
241-
const parts = getTimeZoneDateTimeParts(date, timeZone);
242-
const zonedAsUtc = Date.UTC(
243-
parts.year,
244-
parts.month - 1,
245-
parts.day,
246-
parts.hour,
247-
parts.minute,
248-
parts.second,
249-
);
250-
251-
return zonedAsUtc - date.getTime();
175+
function getDatabricksCacheKey(config: DatabricksConfig) {
176+
return [
177+
config.serverHostname,
178+
config.httpPath,
179+
config.warehouseId,
180+
MAX_RANGE_DAYS,
181+
metricNames.join(","),
182+
].join("|");
252183
}
253184

254-
function getTimeZoneDateTimeParts(date: Date, timeZone: string) {
255-
const parts = new Intl.DateTimeFormat("en-US", {
256-
day: "2-digit",
257-
hour: "2-digit",
258-
hourCycle: "h23",
259-
minute: "2-digit",
260-
month: "2-digit",
261-
second: "2-digit",
262-
timeZone,
263-
year: "numeric",
264-
}).formatToParts(date);
265-
const valueByType = new Map(
266-
parts.map((part) => [part.type, Number(part.value)]),
267-
);
185+
function getSuccessCacheControl() {
186+
if (!IS_PRODUCTION) {
187+
return NO_STORE_CACHE_CONTROL;
188+
}
268189

269-
return {
270-
day: valueByType.get("day") ?? 1,
271-
hour: valueByType.get("hour") ?? 0,
272-
minute: valueByType.get("minute") ?? 0,
273-
month: valueByType.get("month") ?? 1,
274-
second: valueByType.get("second") ?? 0,
275-
year: valueByType.get("year") ?? 1970,
276-
};
190+
return [
191+
"public",
192+
"max-age=0",
193+
`s-maxage=${DATABRICKS_CACHE_REVALIDATE_SECONDS}`,
194+
`stale-while-revalidate=${EDGE_STALE_SECONDS}`,
195+
].join(", ");
277196
}
278197

279198
function getConfigErrorResponse(

0 commit comments

Comments
 (0)