mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
feat: shared cache for next caching (#2426)
This commit is contained in:
committed by
GitHub
parent
04e43725d1
commit
ee15c2676c
@@ -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:
|
||||
|
||||
27
apps/web/app/api/internal/cache/client.ts
vendored
27
apps/web/app/api/internal/cache/client.ts
vendored
@@ -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;
|
||||
};
|
||||
};
|
||||
21
apps/web/app/api/internal/cache/route.ts
vendored
21
apps/web/app/api/internal/cache/route.ts
vendored
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user