feat: shared cache for next caching (#2426)

This commit is contained in:
Shubham Palriwala
2024-04-12 00:35:49 +05:30
committed by GitHub
parent 04e43725d1
commit ee15c2676c
11 changed files with 61 additions and 73 deletions

View File

@@ -172,3 +172,9 @@ ENTERPRISE_LICENSE_KEY=
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL:
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
# REDIS_HTTP_URL:

View File

@@ -1,27 +0,0 @@
import { createClient } from "redis";
import { REDIS_CLIENT_URL } from "@formbricks/lib/constants";
const client = createClient({
url: REDIS_CLIENT_URL!,
});
client.on("error", (err) => console.error("Redis Client Error", err));
client.connect();
type Options = {
interval: number;
allowedPerInterval: number;
};
export const redisRateLimiter = (options: Options) => {
return async (token: string) => {
const tokenCount = await client.INCR(token);
if (tokenCount === 1) {
await client.EXPIRE(token, options.interval / 1000);
}
if (tokenCount > options.allowedPerInterval) {
throw new Error("Rate limit exceeded for IP: " + token);
}
return;
};
};

View File

@@ -1,21 +0,0 @@
import { redisRateLimiter } from "@/app/api/internal/cache/client";
import { responses } from "@/app/lib/api/response";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token");
const interval = parseInt(request.nextUrl.searchParams.get("interval") ?? "0");
const allowedPerInterval = parseInt(request.nextUrl.searchParams.get("allowedPerInterval") ?? "0");
if (!token) {
return responses.notAuthenticatedResponse();
}
try {
const rateLimiter = redisRateLimiter({ interval, allowedPerInterval });
await rateLimiter(token);
return responses.successResponse({ rateLimitExceeded: false }, true);
} catch (e) {
return responses.successResponse({ rateLimitExceeded: true }, true);
}
}

View File

@@ -1,6 +1,6 @@
import { LRUCache } from "lru-cache";
import { REDIS_CLIENT_URL, WEBAPP_URL } from "@formbricks/lib/constants";
import { REDIS_HTTP_URL } from "@formbricks/lib/constants";
type Options = {
interval: number;
@@ -22,22 +22,27 @@ const inMemoryRateLimiter = (options: Options) => {
};
};
const redisRateLimiter = (options: Options) => {
return async (token: string) => {
const tokenCountResponse = await fetch(
`${WEBAPP_URL}/api/internal/cache?token=${token}&interval=${options.interval}&allowedPerInterval=${options.allowedPerInterval}`
);
const {
data: { rateLimitExceeded },
} = await tokenCountResponse.json();
if (!tokenCountResponse.ok || rateLimitExceeded) {
throw new Error("Rate limit exceeded for IP: " + token);
const redisRateLimiter = (options: Options) => async (token: string) => {
try {
const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`);
if (!tokenCountResponse.ok) {
console.error("Failed to increment token count in Redis", tokenCountResponse);
return;
}
};
const { INCR } = await tokenCountResponse.json();
if (INCR === 1) {
await fetch(`${REDIS_HTTP_URL}/EXPIRE/${token}/${options.interval}`);
} else if (INCR > options.allowedPerInterval) {
throw new Error();
}
} catch (e) {
throw new Error("Rate limit exceeded for IP: " + token);
}
};
export default function rateLimit(options: Options) {
if (REDIS_CLIENT_URL) {
if (REDIS_HTTP_URL) {
return redisRateLimiter(options);
} else {
return inMemoryRateLimiter(options);

View File

@@ -5,11 +5,13 @@ import { createClient } from "redis";
CacheHandler.onCreation(async () => {
let redisHandler;
if (process.env.REDIS_CLIENT_URL) {
if (process.env.REDIS_URL) {
const client = createClient({
url: process.env.REDIS_CLIENT_URL,
url: process.env.REDIS_URL,
});
client.on("error", (e) => {
console.error("Error in conncting to Redis client", e);
});
client.on("error", () => {});
await client.connect();
redisHandler = createRedisHandler({

View File

@@ -115,7 +115,7 @@ services:
GOOGLE_CLIENT_ID: *google_client_id
GOOGLE_CLIENT_SECRET: *google_client_secret
CRON_SECRET: *cron_secret
REDIS_CLIENT_URL: *redis_url
REDIS_URL: *redis_url
volumes:
- uploads:/home/nextjs/apps/web/uploads/

View File

@@ -79,8 +79,11 @@ x-environment: &environment
# Uncomment and set to 1 to skip onboarding for new users
# ONBOARDING_DISABLED: 1
# The below is used for Rate Limiting & Next Caching (uses In-Memory Next Cache if not provided)
# REDIS_CLIENT_URL:
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL:
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided)
# REDIS_HTTP_URL:
services:
postgres:

View File

@@ -29,8 +29,9 @@ registry:
env:
# clear:
# DB_HOST: 192.168.0.2
# clear:
# REDIS_CLIENT_URL: redis://default:password@172.31.40.79:6379
clear:
REDIS_URL: redis://default:password@172.31.40.79:6379
REDIS_HTTP_URL: http://172.31.40.79:7379
secret:
- IS_FORMBRICKS_CLOUD
- WEBAPP_URL
@@ -170,6 +171,21 @@ accessories:
port: "172.31.40.79:6379:6379"
directories:
- data:/data
webdis:
image: nicolas/webdis:0.1.22
host: 18.196.187.144
cmd: >
sh -c "
wget -O /usr/local/bin/webdis.json https://github.com/nicolasff/webdis/raw/0.1.22/webdis.json &&
awk '/\"redis_host\":/ {print \"\\t\\\"redis_host\\\": \\\"172.31.40.79\\\",\"; next} /\"logfile\":/ {print \"\\t\\\"logfile\\\": \\\"/dev/stderr\\\"\"; next} {print}' /usr/local/bin/webdis.json > /usr/local/bin/webdis_modified.json &&
mv /usr/local/bin/webdis_modified.json /usr/local/bin/webdis.json &&
/usr/local/bin/webdis /usr/local/bin/webdis.json"
port: "172.31.40.79:7379:7379"
directories:
- data:/data
pgbouncer:
image: edoburu/pgbouncer:latest
host: 18.196.187.144

View File

@@ -176,7 +176,8 @@ export const DEBUG = env.DEBUG === "1";
// Enterprise License constant
export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;
export const REDIS_CLIENT_URL = env.REDIS_CLIENT_URL;
export const REDIS_URL = env.REDIS_URL;
export const REDIS_HTTP_URL = env.REDIS_HTTP_URL;
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
export const CUSTOMER_IO_SITE_ID = env.CUSTOMER_IO_SITE_ID;

View File

@@ -52,7 +52,8 @@ export const env = createEnv({
OIDC_SIGNING_ALGORITHM: z.string().optional(),
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
ONBOARDING_DISABLED: z.enum(["1", "0"]).optional(),
REDIS_CLIENT_URL: z.string().optional(),
REDIS_URL: z.string().optional(),
REDIS_HTTP_URL: z.string().optional(),
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
PRIVACY_URL: z
.string()
@@ -158,7 +159,8 @@ export const env = createEnv({
OIDC_ISSUER: process.env.OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM: process.env.OIDC_SIGNING_ALGORITHM,
ONBOARDING_DISABLED: process.env.ONBOARDING_DISABLED,
REDIS_CLIENT_URL: process.env.REDIS_CLIENT_URL,
REDIS_URL: process.env.REDIS_URL,
REDIS_HTTP_URL: process.env.REDIS_HTTP_URL,
PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED,
PRIVACY_URL: process.env.PRIVACY_URL,
RATE_LIMITING_DISABLED: process.env.RATE_LIMITING_DISABLED,

View File

@@ -118,7 +118,8 @@
"PLAYWRIGHT_CI",
"PRIVACY_URL",
"RATE_LIMITING_DISABLED",
"REDIS_CLIENT_URL",
"REDIS_URL",
"REDIS_HTTP_URL",
"S3_ACCESS_KEY",
"S3_SECRET_KEY",
"S3_REGION",