Skip to content

Commit

Permalink
perf: Create view instead of 2 SQL calls
Browse files Browse the repository at this point in the history
  • Loading branch information
amaury1093 committed Dec 12, 2023
1 parent 41151b9 commit ca811a6
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 56 deletions.
42 changes: 26 additions & 16 deletions src/pages/api/v0/check_email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const POST = async (
if (sentResponse) {
return;
}
let d1 = performance.now() - startTime;
const d1 = performance.now() - startTime;
console.log(`[🐢] checkUserInDB: ${Math.round(d1)}ms`);

try {
Expand Down Expand Up @@ -66,7 +66,7 @@ const POST = async (
async function (msg) {
if (msg?.properties.correlationId === verificationId) {
const output = JSON.parse(msg.content.toString());
let d5 = performance.now() - startTime;
const d5 = performance.now() - startTime;
console.log(
`[🐢] Got consume message: ${Math.round(d5)}ms`
);
Expand Down Expand Up @@ -94,29 +94,39 @@ const POST = async (
res.status(response.status).json(response.error);
return;
}
let d6 = performance.now() - startTime;
const d6 = performance.now() - startTime;
console.log(`[🐢] Add to supabase: ${Math.round(d6)}ms`);

await ch1.close();
let d7 = performance.now() - startTime;
console.log(`[🐢] ch1.close: ${Math.round(d7)}ms`);
await conn.close();
let d8 = performance.now() - startTime;
console.log(`[🐢] conn.close: ${Math.round(d8)}ms`);

await updateSendinblue(user);
let d9 = performance.now() - startTime;
console.log(`[🐢] updateSendinblue: ${Math.round(d9)}ms`);
// Cleanup
await Promise.all([
updateSendinblue(user).then(() => {
const d8 = performance.now() - startTime;
console.log(
`[🐢] updateSendinblue: ${Math.round(d8)}ms`
);
}),
ch1
.close()
.then(() => conn.close())
.then(() => {
const d7 = performance.now() - startTime;
console.log(
`[🐢] ch1.close: ${Math.round(d7)}ms`
);
}),
]);

res.status(200).json(output);
const d9 = performance.now() - startTime;
console.log(`[🐢] Final response: ${Math.round(d9)}ms`);
}
},
{
noAck: true,
}
);

let d2 = performance.now() - startTime;
const d2 = performance.now() - startTime;
console.log(`[🐢] AMQP setup: ${Math.round(d2)}ms`);

const verifMethod = await getVerifMethod(req.body as CheckEmailInput);
Expand All @@ -130,7 +140,7 @@ const POST = async (
// reputation.
verifMethod === "Api" ? "Headless" : verifMethod
}`;
let d3 = performance.now() - startTime;
const d3 = performance.now() - startTime;
console.log(`[🐢] getVerifMethod: ${Math.round(d3)}ms`);

await ch1.assertQueue(queueName, {
Expand All @@ -151,7 +161,7 @@ const POST = async (
replyTo: replyQ.queue,
}
);
let d4 = performance.now() - startTime;
const d4 = performance.now() - startTime;
console.log(`[🐢] sendToQueue: ${Math.round(d4)}ms`);

setTimeout(() => {
Expand Down
41 changes: 21 additions & 20 deletions src/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import { NextApiRequest, NextApiResponse } from "next";
import { RateLimiterRes } from "rate-limiter-flexible";

import { subApiMaxCalls } from "./subs";
import { SupabaseUser } from "./supabaseClient";
import {
getActiveSubscription,
getApiUsageServer,
getUserByApiToken,
} from "./supabaseServer";
import { SupabaseSubscription, SupabaseUser } from "./supabaseClient";
import { getSubAndCalls, getUserByApiToken } from "./supabaseServer";

// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
Expand Down Expand Up @@ -78,30 +74,35 @@ export async function checkUserInDB(
return { sentResponse: true };
}

// Safe to type cast here, as we only need the `id` field below.
const authUser = { id: user.id } as User;

// TODO instead of doing another round of network call, we should do a
// join for subscriptions and API calls inside getUserByApiToken.
const sub = await getActiveSubscription(authUser);
const used = await getApiUsageServer(user, sub);
const subAndCalls = await getSubAndCalls(user.id);

// Set rate limit headers.
const now = new Date();
const nextReset = sub
? typeof sub.current_period_end === "string"
? parseISO(sub.current_period_end)
: sub.current_period_end
const nextReset = subAndCalls.subscription_id
? typeof subAndCalls.current_period_end === "string"
? parseISO(subAndCalls.current_period_end)
: subAndCalls.current_period_end
: addMonths(now, 1);
const msDiff = differenceInMilliseconds(nextReset, now);
const max = subApiMaxCalls(sub);
const max = subApiMaxCalls({
prices: {
products: {
id: subAndCalls.product_id,
},
},
} as SupabaseSubscription);
setRateLimitHeaders(
res,
new RateLimiterRes(max - used - 1, msDiff, used, undefined), // 1st arg has -1, because we just consumed 1 email.
new RateLimiterRes(
max - subAndCalls.number_of_calls - 1,
msDiff,
subAndCalls.number_of_calls,
undefined
), // 1st arg has -1, because we just consumed 1 email.
max
);

if (used > max) {
if (subAndCalls.number_of_calls > max) {
res.status(429).json({
error: "Too many requests this month. Please upgrade your Reacher plan to make more requests.",
});
Expand Down
3 changes: 1 addition & 2 deletions src/util/supabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ export function updateUserName(
.eq("id", user.id);
}

// Get the api calls of a user in the past month. Same as
// `getApiUsageServer`, but for client usage.
// Get the api calls of a user in the past month.
export async function getApiUsageClient(
user: User,
subscription: SupabaseSubscription | null | undefined
Expand Down
35 changes: 17 additions & 18 deletions src/util/supabaseServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,26 @@ export async function getActiveSubscription(
return data?.[0];
}

// Get the api calls of a user in the past month. Same as
// `getApiUsageClient`, but for server usage.
export async function getApiUsageServer(
user: SupabaseUser,
subscription: SupabaseSubscription | null | undefined
): Promise<number> {
const { count, error } = await supabaseAdmin
.from<SupabaseCall>("calls")
.select("*", { count: "exact" })
.eq("user_id", user.id)
.gt("created_at", getUsageStartDate(subscription).toISOString());
interface SubAndCalls {
user_id: string;
subscription_id: string | null;
product_id: string | null;
email: string;
current_period_start: string | Date;
current_period_end: string | Date;
number_of_calls: number;
}

export async function getSubAndCalls(userId: string): Promise<SubAndCalls> {
const { data, error } = await supabaseAdmin
.from<SubAndCalls>("sub_and_calls")
.select("*")
.eq("user_id", userId)
.single();
if (error) {
console.log("getSubAndCalls error", error);
throw error;
}

if (count === null) {
throw new Error(
`Got null count in getApiUsageServer for user ${user.id}.`
);
}

return count;
return data;
}
49 changes: 49 additions & 0 deletions supabase/migrations/20231212094633_performance.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
This SQL code creates a view called "subs_and_calls" that combines information from three tables: "auth.users", "subscriptions", and "calls".
The view includes the following columns:
- u.id: The user ID from the "auth.users" table.
- u.email: The user's email address from the "auth.users" table.
- s.id AS subscription_id: The subscription ID from the "subscriptions" table.
- s.current_period_start: The start date of the current subscription period from the "subscriptions" table.
- s.current_period_end: The end date of the current subscription period from the "subscriptions" table.
- COUNT(c.id) AS number_of_calls: The number of calls made by the user, calculated by counting the records in the "calls" table.
The view is created using LEFT JOINs to ensure that all users are included, even if they don't have an active subscription or any calls.
The JOIN conditions filter the subscriptions based on their status being 'active' and the calls based on their creation date falling within the current subscription period or within the last month.
The result is grouped by the user ID, subscription ID, and current subscription period end date.
This view can be used to retrieve information about users, their subscriptions, and the number of calls they have made.
*/
DROP VIEW sub_and_calls;
CREATE VIEW sub_and_calls
WITH (security_invoker = TRUE)
AS
SELECT
u.id as user_id,
s.id AS subscription_id,
s.current_period_start,
s.current_period_end,
pro.id as product_id,
COUNT(c.id) AS number_of_calls
FROM
users u
LEFT JOIN
subscriptions s ON u.id = s.user_id
AND s.status = 'active'
LEFT JOIN
prices pri ON pri.id = s.price_id
LEFT JOIN
products pro on pro.id = pri.product_id
LEFT JOIN
calls c ON u.id = c.user_id
AND (
(s.current_period_start IS NOT NULL AND s.current_period_end IS NOT NULL AND c.created_at BETWEEN s.current_period_start AND s.current_period_end)
OR
(c.created_at >= NOW() - INTERVAL '1 MONTH')
)
GROUP BY
u.id, s.id, s.current_period_end, pro.id;

-- Create in index on api_token to speed up the query.
CREATE INDEX api_token_index ON public.users (api_token);

0 comments on commit ca811a6

Please sign in to comment.