Skip to content

Commit 3cb5c8a

Browse files
authored
Merge pull request #108 from codegasms/feat/database-caching
Cache database queries and collect metrics
2 parents 4bc7afa + 8ac14d1 commit 3cb5c8a

File tree

14 files changed

+620
-127
lines changed

14 files changed

+620
-127
lines changed

app/api/metrics/route.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse } from "next/server";
22
import { metrics } from "@/lib/metrics";
3+
import { cacheMetrics } from "@/lib/metrics/cache";
34

45
/**
56
* @swagger
@@ -22,7 +23,8 @@ import { metrics } from "@/lib/metrics";
2223
* # TYPE http_request_duration_ms histogram
2324
* http_request_duration_ms_bucket{method="GET",path="/api/metrics",le="10"} 1
2425
*/
25-
export const runtime = "edge";
26+
/*
27+
export const runtime = "edge";
2628
2729
export async function GET() {
2830
return new NextResponse(metrics.getMetrics(), {
@@ -31,3 +33,80 @@ export async function GET() {
3133
},
3234
});
3335
}
36+
*/
37+
38+
export async function GET() {
39+
try {
40+
const report = cacheMetrics.generateReport();
41+
42+
return new Response(
43+
JSON.stringify(
44+
{
45+
timestamp: new Date().toISOString(),
46+
metrics: {
47+
cached: {
48+
averageResponseTime: report.withCache.avg,
49+
p95ResponseTime: report.withCache.p95,
50+
hits: report.withCache.hits,
51+
misses: report.withCache.misses,
52+
hitRate:
53+
report.withCache.hits /
54+
(report.withCache.hits + report.withCache.misses),
55+
},
56+
uncached: {
57+
averageResponseTime: report.withoutCache.avg,
58+
p95ResponseTime: report.withoutCache.p95,
59+
},
60+
impact: {
61+
dbQueriesSaved: report.dbQueriesSaved,
62+
/*
63+
memoryDelta: {
64+
withCache: report.memoryUsage.withCache,
65+
withoutCache: report.memoryUsage.withoutCache,
66+
difference:
67+
report.memoryUsage.withCache -
68+
report.memoryUsage.withoutCache,
69+
},
70+
*/
71+
},
72+
},
73+
},
74+
null,
75+
2,
76+
),
77+
{
78+
status: 200,
79+
headers: {
80+
"Content-Type": "application/json",
81+
"Cache-Control": "no-store", // Don't cache metrics response
82+
},
83+
},
84+
);
85+
} catch (error) {
86+
if (error instanceof Error && error.message.includes("ENOENT")) {
87+
// No metrics file found
88+
return new Response(
89+
JSON.stringify({
90+
error: "No metrics collected yet",
91+
timestamp: new Date().toISOString(),
92+
}),
93+
{
94+
status: 404,
95+
headers: { "Content-Type": "application/json" },
96+
},
97+
);
98+
}
99+
100+
console.error("Failed to generate metrics report:", error);
101+
return new Response(
102+
JSON.stringify({
103+
error: "Failed to generate metrics report",
104+
timestamp: new Date().toISOString(),
105+
}),
106+
{
107+
status: 500,
108+
headers: { "Content-Type": "application/json" },
109+
},
110+
);
111+
}
112+
}

app/api/orgs/[orgId]/contests/service.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { contests, problems, contestProblems } from "@/db/schema";
44
import { createContestSchema, updateContestSchema } from "@/lib/validations";
55
import { and, count, eq, asc } from "drizzle-orm";
66
import { getProblemIdFromCode } from "../problems/service";
7+
import { CACHE_TTL } from "@/db/redis";
8+
import { withDataCache } from "@/lib/cache/utils";
79

810
export async function createContest(
911
orgId: number,
@@ -136,41 +138,52 @@ export async function getOrgContests(
136138
}
137139

138140
export async function getContestByNameId(orgId: number, nameId: string) {
139-
const contest = await db.query.contests.findFirst({
140-
where: and(eq(contests.organizerId, orgId), eq(contests.nameId, nameId)),
141-
});
141+
return withDataCache(
142+
`contest:${orgId}:${nameId}`,
143+
async () => {
144+
const contest = await db.query.contests.findFirst({
145+
where: and(
146+
eq(contests.organizerId, orgId),
147+
eq(contests.nameId, nameId),
148+
eq(contests.organizerKind, "org"),
149+
),
150+
});
142151

143-
if (!contest) {
144-
throw new Error("Contest not found");
145-
}
152+
if (!contest) {
153+
throw new Error("Contest not found");
154+
}
146155

147-
// Fetch problems for the contest
148-
const problemsData = await db.query.contestProblems.findMany({
149-
where: eq(contestProblems.contestId, contest.id),
150-
orderBy: (contestProblems, { asc }) => [asc(contestProblems.order)],
151-
});
156+
// Fetch problems for the contest
157+
const problemsData = await db.query.contestProblems.findMany({
158+
where: eq(contestProblems.contestId, contest.id),
159+
orderBy: (contestProblems, { asc }) => [asc(contestProblems.order)],
160+
});
152161

153-
// For each problem ID, get the problem code
154-
const problemCodes = await Promise.all(
155-
problemsData.map(async (p) => {
156-
try {
157-
const problem = await db.query.problems.findFirst({
158-
where: eq(problems.id, p.problemId),
159-
columns: { code: true },
160-
});
161-
return problem?.code || "";
162-
} catch (error) {
163-
return "";
164-
}
165-
}),
166-
);
162+
// For each problem ID, get the problem code
163+
const problemCodes = await Promise.all(
164+
problemsData.map(async (p) => {
165+
try {
166+
const problem = await db.query.problems.findFirst({
167+
where: eq(problems.id, p.problemId),
168+
columns: { code: true },
169+
});
170+
return problem?.code || "";
171+
} catch (error) {
172+
return "";
173+
}
174+
}),
175+
);
167176

168-
const problemsList = problemCodes.filter((code) => code).join(",");
177+
const problemsList = problemCodes.filter((code) => code).join(",");
169178

170-
return {
171-
...contest,
172-
problems: problemsList,
173-
};
179+
return {
180+
...contest,
181+
problems: problemsList,
182+
};
183+
},
184+
CACHE_TTL.MEDIUM,
185+
3 + 2,
186+
);
174187
}
175188

176189
export async function updateContest(

app/api/orgs/[orgId]/problems/[problemId]/service.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,34 @@ import { problems, contestProblems, testCases } from "@/db/schema";
33
import { eq, and } from "drizzle-orm";
44
import { z } from "zod";
55
import { updateProblemSchema } from "@/lib/validations";
6+
import { withDataCache } from "@/lib/cache/utils";
7+
import { CACHE_TTL } from "@/db/redis";
68

79
export async function getProblem(orgId: number, code: string) {
8-
const problem = await db.query.problems.findFirst({
9-
where: and(eq(problems.orgId, orgId), eq(problems.code, code)),
10-
with: {
11-
testCases: {
12-
where: eq(testCases.kind, "example"),
13-
columns: {
14-
input: true,
15-
output: true,
10+
return withDataCache(
11+
`org:problem:${orgId}:${code}`,
12+
async () => {
13+
const problem = await db.query.problems.findFirst({
14+
where: and(eq(problems.orgId, orgId), eq(problems.code, code)),
15+
with: {
16+
testCases: {
17+
where: eq(testCases.kind, "example"),
18+
columns: {
19+
input: true,
20+
output: true,
21+
},
22+
},
1623
},
17-
},
18-
},
19-
});
24+
});
2025

21-
if (!problem) {
22-
throw new Error("Problem not found");
23-
}
26+
if (!problem) {
27+
throw new Error("Problem not found");
28+
}
2429

25-
return problem;
30+
return problem;
31+
},
32+
CACHE_TTL.LONG,
33+
);
2634
}
2735

2836
export async function updateProblem(

app/api/orgs/[orgId]/problems/service.ts

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { problems, testCases } from "@/db/schema";
33
import { z } from "zod";
44
import { createProblemSchema, createTestCaseSchema } from "@/lib/validations";
55
import { eq } from "drizzle-orm";
6+
import { withDataCache } from "@/lib/cache/utils";
7+
import { CACHE_TTL } from "@/db/redis";
68

79
export async function createProblem(
810
orgId: number,
@@ -37,47 +39,54 @@ export async function createProblem(
3739
}
3840

3941
export async function getOrgProblems(orgId: number) {
40-
const problemsWithTestCases = await db
41-
.select({
42-
problem: problems,
43-
testCase: {
44-
input: testCases.input,
45-
output: testCases.output,
46-
kind: testCases.kind,
47-
},
48-
})
49-
.from(problems)
50-
.leftJoin(testCases, eq(testCases.problemId, problems.id))
51-
.where(eq(problems.orgId, orgId));
42+
return withDataCache(
43+
`org:problems:${orgId}`,
44+
async () => {
45+
const problemsWithTestCases = await db
46+
.select({
47+
problem: problems,
48+
testCase: {
49+
input: testCases.input,
50+
output: testCases.output,
51+
kind: testCases.kind,
52+
},
53+
})
54+
.from(problems)
55+
.leftJoin(testCases, eq(testCases.problemId, problems.id))
56+
.where(eq(problems.orgId, orgId));
5257

53-
// Group test cases by problem
54-
const groupedProblems = problemsWithTestCases.reduce(
55-
(acc, row) => {
56-
const { problem, testCase } = row;
57-
const problemId = problem.id;
58+
// Group test cases by problem
59+
const groupedProblems = problemsWithTestCases.reduce(
60+
(acc, row) => {
61+
const { problem, testCase } = row;
62+
const problemId = problem.id;
5863

59-
if (!acc[problemId]) {
60-
acc[problemId] = {
61-
...problem,
62-
createdAt: problem.createdAt.toISOString(),
63-
testCases: [],
64-
};
65-
}
64+
if (!acc[problemId]) {
65+
acc[problemId] = {
66+
...problem,
67+
createdAt: problem.createdAt.toISOString(),
68+
testCases: [],
69+
};
70+
}
6671

67-
if (testCase !== null && testCase.input !== null) {
68-
acc[problemId].testCases.push({
69-
input: testCase.input,
70-
output: testCase.output,
71-
kind: testCase.kind ?? "test",
72-
});
73-
}
72+
if (testCase !== null && testCase.input !== null) {
73+
acc[problemId].testCases.push({
74+
input: testCase.input,
75+
output: testCase.output,
76+
kind: testCase.kind ?? "test",
77+
});
78+
}
7479

75-
return acc;
80+
return acc;
81+
},
82+
{} as Record<number, any>,
83+
);
84+
85+
return Object.values(groupedProblems);
7686
},
77-
{} as Record<number, any>,
87+
CACHE_TTL.LONG,
88+
3,
7889
);
79-
80-
return Object.values(groupedProblems);
8190
}
8291

8392
export async function getProblemIdFromCode(

app/api/orgs/[orgId]/route.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { NextRequest, NextResponse } from "next/server";
1+
import { type NextRequest, NextResponse } from "next/server";
22
import { db } from "@/db/drizzle";
33
import { orgs } from "@/db/schema";
44
import { eq } from "drizzle-orm";
55
import { z } from "zod";
66
import { getOrgIdFromNameId } from "../../service";
7+
import { getOrgByOrgId } from "./service";
78

89
const updateOrgSchema = z.object({
910
name: z.string().optional(),
@@ -24,9 +25,7 @@ export async function GET(
2425
);
2526
}
2627

27-
const org = await db.query.orgs.findFirst({
28-
where: eq(orgs.id, orgId),
29-
});
28+
const org = getOrgByOrgId(orgId);
3029

3130
return org
3231
? NextResponse.json(org)

app/api/orgs/[orgId]/service.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { db } from "@/db/drizzle";
2+
import { CACHE_TTL } from "@/db/redis";
3+
import { orgs } from "@/db/schema";
4+
import { withDataCache } from "@/lib/cache/utils";
5+
import { eq } from "drizzle-orm";
6+
7+
export function getOrgByOrgId(orgId: number) {
8+
return withDataCache(
9+
`org:${orgId}`,
10+
async () => {
11+
const org = await db.query.orgs.findFirst({
12+
where: eq(orgs.id, orgId),
13+
});
14+
return org;
15+
},
16+
CACHE_TTL.MEDIUM,
17+
);
18+
}

0 commit comments

Comments
 (0)